Repository: mkdocs/mkdocs

Issue:
High CPU Load
A have High CPU Load (> 20 %) with MkDocs + Material on Lenovo T460s (i5-6200U, 20 GB Ram). Notebook is hot, fan is humming and battery discharges quickly. 

High load occurs during `mkdocs serve`. If I use `mkdocs serve --no-livereload`, load is OK.

High load happens even with minimal content.

SITE-TEST:
```
    docs:
        index.md
    mkdoc.yml
```

index.md:
```
# Hello World
```

mkdoc.yml
```
site_name: Test
theme:
    name: material
    language: cs
```

Load is normal, if I remove theme material.

My enviroment:

```
C:\Users\petrm>python --version
Python 3.8.2
```

```
C:\Users\petrm>python -m pip show mkdocs
Name: mkdocs
Version: 1.1
Summary: Project documentation with Markdown.
Home-page: https://www.mkdocs.org
Author: Tom Christie
Author-email: tom@tomchristie.com
License: BSD
Location: c:\users\petrm\appdata\roaming\python\python38\site-packages
Requires: PyYAML, livereload, lunr, Markdown, click, tornado, Jinja2
Required-by: mkdocs-material
```

```
C:\Users\petrm>python -m pip show mkdocs-material
Name: mkdocs-material
Version: 5.1.4
Summary: A Material Design theme for MkDocs
Home-page: https://squidfunk.github.io/mkdocs-material/
Author: Martin Donath
Author-email: martin.donath@squidfunk.com
License: MIT
Location: c:\users\petrm\appdata\roaming\python\python38\site-packages
Requires: pymdown-extensions, mkdocs, Pygments, mkdocs-material-extensions, markdown
Required-by: mkdocs-material-extensions
```

```
C:\Users\petrm>systeminfo

Host Name:                 T460S-WIN
OS Name:                   Microsoft Windows 10 Pro
OS Version:                10.0.18362 N/A Build 18362
OS Manufacturer:           Microsoft Corporation
OS Configuration:          Standalone Workstation
OS Build Type:             Multiprocessor Free
Registered Owner:          Uživatel systému Windows
Registered Organization:
Product ID:                00342-50620-20329-AAOEM
Original Install Date:     29.10.2019, 23:22:13
System Boot Time:          22.04.2020, 12:13:28
System Manufacturer:       LENOVO
System Model:              20FAS47X00
System Type:               x64-based PC
Processor(s):              1 Processor(s) Installed.
                           [01]: Intel64 Family 6 Model 78 Stepping 3 GenuineIntel ~2300 Mhz
BIOS Version:              LENOVO N1CET45W (1.13 ), 02.06.2016
Windows Directory:         C:\WINDOWS
System Directory:          C:\WINDOWS\system32
Boot Device:               \Device\HarddiskVolume1
System Locale:             cs;Čeština
Input Locale:              cs;Čeština
Time Zone:                 (UTC+01:00) Praha, Bratislava, Budapešť, Bělehrad, Lublaň
Total Physical Memory:     19 907 MB
Available Physical Memory: 14 060 MB
Virtual Memory: Max Size:  22 851 MB
Virtual Memory: Available: 16 008 MB
Virtual Memory: In Use:    6 843 MB
Page File Location(s):     C:\pagefile.sys
Domain:                    WORKGROUP
Logon Server:              \\T460S-WIN
```


Discussion:
Related to #2061, first discussed in squidfunk/mkdocs-material#1672
> If I use `mkdocs serve --no-livereload`, load is OK.
>
> Load is normal, if I remove theme material.

Those are the two factors. The material theme has a lot of files, and watching a lot of files puts extra strain on the system. Especially if the system doesn't have an API for watching files. While Windows does have an API, my understanding is that it is less performant than Linux and Mac and you should expect more strain.

Overall, there is not much we can do. We only provide a light wrapper around [lepture/python-livereload][1]. Any performance issues with the watcher should be addressed there. The theme could reduce the number of files, but then you would loose the features it offers (besides that would be something to discuss with the Material devs, not here). That only leaves #2061 as something we can do. Therefore, I am closing this as a duplicate.

[1]: https://github.com/lepture/python-livereload
Wouldn't it solve the problem if we exclude theme files from `mkdocs serve`?
> Wouldn't it solve the problem if we exclude theme files from `mkdocs serve`?

That is certainly an option. However, when I'm working on a theme (editing templates or CSS) I want to have the theme files watched. Of course, I'm only ever working on smaller themes with only a few files, so it's a non-issue for me.

Perhaps the default should be to not watch theme files. Then we could add an option to include theme files for those occasions were it is desired.
That sounds like a great idea. I think for most of authors, watching theme files isn’t necessary. So I guess this could be implemented as an enhancement?

Resolving patch:
diff --git a/mkdocs/__init__.py b/mkdocs/__init__.py
--- a/mkdocs/__init__.py
+++ b/mkdocs/__init__.py
@@ -2,4 +2,4 @@
 
 
 # For acceptable version formats, see https://www.python.org/dev/peps/pep-0440/
-__version__ = '1.1.2'
+__version__ = '1.2.dev1'
diff --git a/mkdocs/__main__.py b/mkdocs/__main__.py
--- a/mkdocs/__main__.py
+++ b/mkdocs/__main__.py
@@ -56,6 +56,8 @@ def __init__(self, log_name='mkdocs', level=logging.INFO):
                     "overrides the value specified in config")
 force_help = "Force the push to the repository."
 ignore_version_help = "Ignore check that build is not being deployed with an older version of MkDocs."
+watch_theme_help = ("Include the theme in list of files to watch for live reloading. "
+                    "Ignored when live reload is not used.")
 
 
 def add_options(opts):
@@ -122,6 +124,7 @@ def cli():
 @click.option('--livereload', 'livereload', flag_value='livereload', help=reload_help, default=True)
 @click.option('--no-livereload', 'livereload', flag_value='no-livereload', help=no_reload_help)
 @click.option('--dirtyreload', 'livereload', flag_value='dirty', help=dirty_reload_help)
+@click.option('--watch-theme', help=watch_theme_help, is_flag=True)
 @common_config_options
 @common_options
 def serve_command(dev_addr, livereload, **kwargs):
diff --git a/mkdocs/commands/serve.py b/mkdocs/commands/serve.py
--- a/mkdocs/commands/serve.py
+++ b/mkdocs/commands/serve.py
@@ -48,7 +48,7 @@ def write_error(self, status_code, **kwargs):
     return WebHandler
 
 
-def _livereload(host, port, config, builder, site_dir):
+def _livereload(host, port, config, builder, site_dir, watch_theme):
 
     # We are importing here for anyone that has issues with livereload. Even if
     # this fails, the --no-livereload alternative should still work.
@@ -69,8 +69,9 @@ def get_web_handlers(self, script):
     server.watch(config['docs_dir'], builder)
     server.watch(config['config_file_path'], builder)
 
-    for d in config['theme'].dirs:
-        server.watch(d, builder)
+    if watch_theme:
+        for d in config['theme'].dirs:
+            server.watch(d, builder)
 
     # Run `serve` plugin events.
     server = config['plugins'].run_event('serve', server, config=config, builder=builder)
@@ -103,7 +104,7 @@ def _static_server(host, port, site_dir):
 
 
 def serve(config_file=None, dev_addr=None, strict=None, theme=None,
-          theme_dir=None, livereload='livereload', **kwargs):
+          theme_dir=None, livereload='livereload', watch_theme=False, **kwargs):
     """
     Start the MkDocs development server
 
@@ -143,7 +144,7 @@ def builder():
         host, port = config['dev_addr']
 
         if livereload in ['livereload', 'dirty']:
-            _livereload(host, port, config, builder, site_dir)
+            _livereload(host, port, config, builder, site_dir, watch_theme)
         else:
             _static_server(host, port, site_dir)
     finally:


Test patch:
diff --git a/mkdocs/tests/cli_tests.py b/mkdocs/tests/cli_tests.py
--- a/mkdocs/tests/cli_tests.py
+++ b/mkdocs/tests/cli_tests.py
@@ -28,7 +28,8 @@ def test_serve_default(self, mock_serve):
             config_file=None,
             strict=None,
             theme=None,
-            use_directory_urls=None
+            use_directory_urls=None,
+            watch_theme=False
         )
 
     @mock.patch('mkdocs.commands.serve.serve', autospec=True)
@@ -57,7 +58,8 @@ def test_serve_dev_addr(self, mock_serve):
             config_file=None,
             strict=None,
             theme=None,
-            use_directory_urls=None
+            use_directory_urls=None,
+            watch_theme=False
         )
 
     @mock.patch('mkdocs.commands.serve.serve', autospec=True)
@@ -73,7 +75,8 @@ def test_serve_strict(self, mock_serve):
             config_file=None,
             strict=True,
             theme=None,
-            use_directory_urls=None
+            use_directory_urls=None,
+            watch_theme=False
         )
 
     @mock.patch('mkdocs.commands.serve.serve', autospec=True)
@@ -89,7 +92,8 @@ def test_serve_theme(self, mock_serve):
             config_file=None,
             strict=None,
             theme='readthedocs',
-            use_directory_urls=None
+            use_directory_urls=None,
+            watch_theme=False
         )
 
     @mock.patch('mkdocs.commands.serve.serve', autospec=True)
@@ -105,7 +109,8 @@ def test_serve_use_directory_urls(self, mock_serve):
             config_file=None,
             strict=None,
             theme=None,
-            use_directory_urls=True
+            use_directory_urls=True,
+            watch_theme=False
         )
 
     @mock.patch('mkdocs.commands.serve.serve', autospec=True)
@@ -121,7 +126,8 @@ def test_serve_no_directory_urls(self, mock_serve):
             config_file=None,
             strict=None,
             theme=None,
-            use_directory_urls=False
+            use_directory_urls=False,
+            watch_theme=False
         )
 
     @mock.patch('mkdocs.commands.serve.serve', autospec=True)
@@ -137,7 +143,8 @@ def test_serve_livereload(self, mock_serve):
             config_file=None,
             strict=None,
             theme=None,
-            use_directory_urls=None
+            use_directory_urls=None,
+            watch_theme=False
         )
 
     @mock.patch('mkdocs.commands.serve.serve', autospec=True)
@@ -153,7 +160,8 @@ def test_serve_no_livereload(self, mock_serve):
             config_file=None,
             strict=None,
             theme=None,
-            use_directory_urls=None
+            use_directory_urls=None,
+            watch_theme=False
         )
 
     @mock.patch('mkdocs.commands.serve.serve', autospec=True)
@@ -169,7 +177,25 @@ def test_serve_dirtyreload(self, mock_serve):
             config_file=None,
             strict=None,
             theme=None,
-            use_directory_urls=None
+            use_directory_urls=None,
+            watch_theme=False
+        )
+
+    @mock.patch('mkdocs.commands.serve.serve', autospec=True)
+    def test_serve_watch_theme(self, mock_serve):
+
+        result = self.runner.invoke(
+            cli.cli, ["serve", '--watch-theme'], catch_exceptions=False)
+
+        self.assertEqual(result.exit_code, 0)
+        mock_serve.assert_called_once_with(
+            dev_addr=None,
+            livereload='livereload',
+            config_file=None,
+            strict=None,
+            theme=None,
+            use_directory_urls=None,
+            watch_theme=True
         )
 
     @mock.patch('mkdocs.config.load_config', autospec=True)