Repository: airspeed-velocity/asv

Issue:
Bug of asv profile with snakeviz (TypeError: '>' not supported between instances of 'float' and 'NoneType')
I was following the [documentation of using asv](https://asv.readthedocs.io/en/stable/using.html) for my project. Everything worked fine, but then I've decided to profile using `asv`, and use a GUI tool for viewing the results.

I could not install `RunSnakeRun` due to syntax error in Python 3 (I'm using Python 3.6). Hence, I've decided to install `snakeviz` because it is also mentioned in the `asv` docs. The installation went well, and I've started the `asv profile` command. 

During the execution of the command, `asv` printed an exception stack trace into the command line (see below). The snakeviz launched successfully, and displayed the results (hopefully) properly. Still, i've decided to report the issue (seems like a tiny bug).

```bash
asv profile --gui snakeviz benchmarks.TimeSuite.time_keys
```
The output in the command line is the following:
```
· Discovering benchmarks
·· Uninstalling from conda-py3.6
·· Building for conda-py3.6
·· Installing into conda-py3.6
· Profile data does not already exist. Running profiler now.
·· Uninstalling from conda-py3.6
·· Building for conda-py3.6
·· Installing into conda-py3.6
·· Benchmarking conda-py3.6
··· Running benchmarks.TimeSuite.time_keys                                                                                          7.66μsException i
n thread Thread-58:
Traceback (most recent call last):
  File "c:\users\a-miasta\appdata\local\continuum\anaconda3\envs\qcodes\lib\threading.py", line 916, in _bootstrap_inner
    self.run()
  File "c:\users\a-miasta\appdata\local\continuum\anaconda3\envs\qcodes\lib\threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "c:\users\a-miasta\appdata\local\continuum\anaconda3\envs\qcodes\lib\site-packages\asv\util.py", line 407, in watcher_run
    if time.time() - start_time[0] > timeout:
TypeError: '>' not supported between instances of 'float' and 'NoneType'
```


Comments:

Patch:
diff --git a/asv/results.py b/asv/results.py
index 76dbe6b..8513b71 100644
--- a/asv/results.py
+++ b/asv/results.py
@@ -4,6 +4,7 @@
 from __future__ import (absolute_import, division, print_function,
                         unicode_literals)
 
+import sys
 import base64
 import os
 import zlib
@@ -405,15 +406,31 @@ class Results(object):
         self._benchmark_version[benchmark_name] = benchmark_version
 
         if 'profile' in result and result['profile']:
-            self._profiles[benchmark_name] = base64.b64encode(
+            profile_data = base64.b64encode(
                 zlib.compress(result['profile']))
+            if sys.version_info[0] >= 3:
+                profile_data = profile_data.decode('ascii')
+            self._profiles[benchmark_name] = profile_data
 
     def get_profile(self, benchmark_name):
         """
         Get the profile data for the given benchmark name.
+
+        Parameters
+        ----------
+        benchmark_name : str
+            Name of benchmark
+
+        Returns
+        -------
+        profile_data : bytes
+            Raw profile data
+
         """
-        return zlib.decompress(
-            base64.b64decode(self._profiles[benchmark_name]))
+        profile_data = self._profiles[benchmark_name]
+        if sys.version_info[0] >= 3:
+            profile_data = profile_data.encode('ascii')
+        return zlib.decompress(base64.b64decode(profile_data))
 
     def has_profile(self, benchmark_name):
         """
diff --git a/asv/util.py b/asv/util.py
index 748b9ba..8e883dd 100644
--- a/asv/util.py
+++ b/asv/util.py
@@ -375,7 +375,9 @@ def check_output(args, valid_return_codes=(0,), timeout=600, dots=True,
         Setting to None ignores all return codes.
 
     timeout : number, optional
-        Kill the process if it lasts longer than `timeout` seconds.
+        Kill the process if it does not produce any output in `timeout`
+        seconds. If `None`, there is no timeout.
+        Default: 10 min
 
     dots : bool, optional
         If `True` (default) write a dot to the console to show
@@ -475,7 +477,7 @@ def check_output(args, valid_return_codes=(0,), timeout=600, dots=True,
         def watcher_run():
             while proc.returncode is None:
                 time.sleep(0.1)
-                if time.time() - start_time[0] > timeout:
+                if timeout is not None and time.time() - start_time[0] > timeout:
                     was_timeout[0] = True
                     proc.send_signal(signal.CTRL_BREAK_EVENT)
 
@@ -522,8 +524,12 @@ def check_output(args, valid_return_codes=(0,), timeout=600, dots=True,
 
             while proc.poll() is None:
                 try:
-                    rlist, wlist, xlist = select.select(
-                        list(fds.keys()), [], [], timeout)
+                    if timeout is None:
+                        rlist, wlist, xlist = select.select(
+                            list(fds.keys()), [], [])
+                    else:
+                        rlist, wlist, xlist = select.select(
+                            list(fds.keys()), [], [], timeout)
                 except select.error as err:
                     if err.args[0] == errno.EINTR:
                         # interrupted by signal handler; try again
@@ -643,7 +649,11 @@ def write_json(path, data, api_version=None):
         data = dict(data)
         data['version'] = api_version
 
-    with long_path_open(path, 'w') as fd:
+    open_kwargs = {}
+    if sys.version_info[0] >= 3:
+        open_kwargs['encoding'] = 'utf-8'
+
+    with long_path_open(path, 'w', **open_kwargs) as fd:
         json.dump(data, fd, indent=4, sort_keys=True)
 
 
@@ -656,7 +666,11 @@ def load_json(path, api_version=None, cleanup=True):
 
     path = os.path.abspath(path)
 
-    with long_path_open(path, 'r') as fd:
+    open_kwargs = {}
+    if sys.version_info[0] >= 3:
+        open_kwargs['encoding'] = 'utf-8'
+
+    with long_path_open(path, 'r', **open_kwargs) as fd:
         content = fd.read()
 
     if cleanup:


Test patch:
diff --git a/test/test_results.py b/test/test_results.py
index 2a7c0fb..d983a4a 100644
--- a/test/test_results.py
+++ b/test/test_results.py
@@ -35,13 +35,13 @@ def test_results(tmpdir):
         values = {
             'suite1.benchmark1': {'result': [float(i * 0.001)], 'stats': [{'foo': 1}],
                                   'samples': [[1,2]], 'number': [6], 'params': [['a']],
-                                  'version': "1"},
+                                  'version': "1", 'profile': b'\x00\xff'},
             'suite1.benchmark2': {'result': [float(i * i * 0.001)], 'stats': [{'foo': 2}],
                                   'samples': [[3,4]], 'number': [7], 'params': [],
-                                  'version': "1"},
+                                  'version': "1", 'profile': b'\x00\xff'},
             'suite2.benchmark1': {'result': [float((i + 1) ** -1)], 'stats': [{'foo': 3}],
                                   'samples': [[5,6]], 'number': [8], 'params': [['c']],
-                                  'version': None}
+                                  'version': None, 'profile': b'\x00\xff'}
         }
 
         for key, val in values.items():
@@ -66,6 +66,7 @@ def test_results(tmpdir):
             assert rr._stats == r._stats
             assert rr._number == r._number
             assert rr._samples == r._samples
+            assert rr._profiles == r._profiles
             assert rr.started_at == r._started_at
             assert rr.ended_at == r._ended_at
             assert rr.benchmark_version == r._benchmark_version
@@ -87,6 +88,9 @@ def test_results(tmpdir):
             assert r2.get_result_stats(bench, bad_params) == [None, None]
             assert r2.get_result_samples(bench, bad_params) == ([None, None], [None, None])
 
+            # Get profile
+            assert r2.get_profile(bench) == b'\x00\xff'
+
         # Check get_result_keys
         mock_benchmarks = {
             'suite1.benchmark1': {'version': '1'},
diff --git a/test/test_subprocess.py b/test/test_subprocess.py
index 5be4fa7..f883ec7 100644
--- a/test/test_subprocess.py
+++ b/test/test_subprocess.py
@@ -122,6 +122,16 @@ print(os.environ['TEST_ASV_BAR'])
     assert output.splitlines() == ['foo', 'bar']
 
 
+def test_no_timeout():
+    # Check that timeout=None is allowed.
+    code = "import time; time.sleep(0.05)"
+    out, err, retcode = util.check_output([sys.executable, "-c", code], timeout=None,
+                                          return_stderr=True)
+    assert out == ''
+    assert err == ''
+    assert retcode == 0
+
+
 # This *does* seem to work, only seems untestable somehow...
 # def test_dots(capsys):
 #     code = r"""
diff --git a/test/test_util.py b/test/test_util.py
index d86167a..19c8ba7 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -269,3 +269,13 @@ def test_is_main_thread():
     thread.join()
 
     assert results == [False]
+
+
+def test_json_non_ascii(tmpdir):
+    non_ascii_data = [{'😼': '難', 'ä': 3}]
+
+    fn = os.path.join(str(tmpdir), "nonascii.json")
+    util.write_json(fn, non_ascii_data)
+    data = util.load_json(fn)
+
+    assert data == non_ascii_data
diff --git a/test/test_workflow.py b/test/test_workflow.py
index 2959f00..5e3983d 100644
--- a/test/test_workflow.py
+++ b/test/test_workflow.py
@@ -102,7 +102,7 @@ def test_run_publish(capfd, basic_conf):
 
     # Tests a typical complete run/publish workflow
     tools.run_asv_with_conf(conf, 'run', "master~5..master", '--steps=2',
-                            '--quick', '--show-stderr',
+                            '--quick', '--show-stderr', '--profile',
                             _machine_file=machine_file)
     text, err = capfd.readouterr()