Repository: plotly/dash

Issue:
[BUG] Latest flask-compress (1.6.0) breaks Dash
`flask-compress` released version 1.6.0 on 2020-10-05 and updating to this version breaks component tests (confirmed in the table) in CI. Since `flask-compress` is a dash requirement and not version locked, this may affect negatively Dash installs in the wild.

- Determine the nature of the break
- Fix it..




Discussion:
@alexcjohnson Got a clean repro of the issue by switching back and forth between `flask-compress` 1.5.0 and 1.6.0

The underlying issue seems to be that 1.6.0 changed the default compression algorithm from`['gzip']` to `['br', 'gzip']`. In doing so they also *do not* provide a way to change the default compression level of Brotli which is set to the maximum value of `11`.

* https://github.com/google/brotli/blob/685d7baea9d8ac7113d14ca6dd1eb4577925af4a/python/brotli.py#L26
* https://github.com/colour-science/flask-compress/blob/master/flask_compress.py#L202

`11` is a very aggressive compression value for Brotli with significant CPU cost - you would want to use it on static resources and do it once / cache the result instead of doing it on the fly like we do.

To give a sense of that cost, callbacks with marginal content (~150 bytes) that would normally return in 10-15ms on the localhost now take 150-200ms to return.

@rpkyle is opening a PR on `flask-compress` to make the default more sensible and to suggest making the compression level for `br`, like `gzip`, customizable.

All of this should have caused performance issues but should not have broken the table tests. The second underlying issue here is that the table tests are brittle when it comes to server slowdowns. For example, `test_tbcp001_copy_paste_callback` is written like this:

```
    assert target.cell(1, 0).get_text() == "0"
    assert target.cell(1, 1).get_text() == "MODIFIED"
```

and would be more resilient written like so:

```
    wait.until(lambda: target.cell(1, 0).get_text() == "0", 3)
    wait.until(lambda: target.cell(1, 1).get_text() == "MODIFIED", 3)
```

Sadly we would also have missed out on the huge performance cost of this update if the tests had been written correctly.

Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -20,6 +20,7 @@
 import flask
 from flask_compress import Compress
 from werkzeug.debug.tbtools import get_current_traceback
+from pkg_resources import get_distribution, parse_version
 
 import plotly
 import dash_renderer
@@ -49,6 +50,8 @@
 from . import _validate
 from . import _watch
 
+_flask_compress_version = parse_version(get_distribution("flask-compress").version)
+
 # Add explicit mapping for map files
 mimetypes.add_type("application/json", ".map", True)
 
@@ -283,6 +286,16 @@ def __init__(
         else:
             raise ValueError("server must be a Flask app or a boolean")
 
+        if (
+            self.server is not None
+            and not hasattr(self.server.config, "COMPRESS_ALGORITHM")
+            and _flask_compress_version >= parse_version("1.6.0")
+        ):
+            # flask-compress==1.6.0 changed default to ['br', 'gzip']
+            # and non-overridable default compression with Brotli is
+            # causing performance issues
+            self.server.config["COMPRESS_ALGORITHM"] = ["gzip"]
+
         base_prefix, routes_prefix, requests_prefix = pathname_configs(
             url_base_pathname, routes_pathname_prefix, requests_pathname_prefix
         )


Test patch: