Jürg Billeter pushed to branch juerg/buffer-size at BuildStream / buildstream
Commits:
-
9e3d3e05
by Angelos Evripiotis at 2019-02-11T07:14:50Z
-
bb6a692d
by Jürg Billeter at 2019-02-11T09:20:34Z
-
1ed63e54
by Angelos Evripiotis at 2019-02-11T09:24:48Z
-
02e48209
by Angelos Evripiotis at 2019-02-11T09:24:48Z
-
4336e3bf
by Angelos Evripiotis at 2019-02-11T09:24:48Z
-
3f6c5000
by Angelos Evripiotis at 2019-02-11T09:24:48Z
-
adde0c94
by Angelos Evripiotis at 2019-02-11T09:24:48Z
-
a66f8379
by Jürg Billeter at 2019-02-11T13:52:54Z
-
aabdf1b9
by Jürg Billeter at 2019-02-11T14:31:39Z
-
649736bc
by Jürg Billeter at 2019-02-11T14:31:39Z
5 changed files:
- CONTRIBUTING.rst
- buildstream/_cas/cascache.py
- buildstream/_includes.py
- buildstream/utils.py
- tests/format/include.py
Changes:
| ... | ... | @@ -1707,26 +1707,13 @@ You can then analyze the results interactively using the 'pstats' module: |
| 1707 | 1707 |
For more detailed documentation of cProfile and 'pstats', see:
|
| 1708 | 1708 |
https://docs.python.org/3/library/profile.html.
|
| 1709 | 1709 |
|
| 1710 |
-For a richer visualisation of the callstack you can try `Pyflame
|
|
| 1711 |
-<https://github.com/uber/pyflame>`_. Once you have followed the instructions in
|
|
| 1712 |
-Pyflame's README to install the tool, you can profile `bst` commands as in the
|
|
| 1713 |
-following example:
|
|
| 1710 |
+For a richer and interactive visualisation of the `.cprofile` files, you can
|
|
| 1711 |
+try `snakeviz <http://jiffyclub.github.io/snakeviz/#interpreting-results>`_.
|
|
| 1712 |
+You can install it with `pip install snakeviz`. Here is an example invocation:
|
|
| 1714 | 1713 |
|
| 1715 |
- pyflame --output bst.flame --trace bst --help
|
|
| 1716 |
- |
|
| 1717 |
-You may see an `Unexpected ptrace(2) exception:` error. Note that the `bst`
|
|
| 1718 |
-operation will continue running in the background in this case, you will need
|
|
| 1719 |
-to wait for it to complete or kill it. Once this is done, rerun the above
|
|
| 1720 |
-command which appears to fix the issue.
|
|
| 1721 |
- |
|
| 1722 |
-Once you have output from pyflame, you can use the ``flamegraph.pl`` script
|
|
| 1723 |
-from the `Flamegraph project <https://github.com/brendangregg/FlameGraph>`_
|
|
| 1724 |
-to generate an .svg image:
|
|
| 1725 |
- |
|
| 1726 |
- ./flamegraph.pl bst.flame > bst-flamegraph.svg
|
|
| 1727 |
- |
|
| 1728 |
-The generated SVG file can then be viewed in your preferred web browser.
|
|
| 1714 |
+ snakeviz bst.cprofile
|
|
| 1729 | 1715 |
|
| 1716 |
+It will then start a webserver and launch a browser to the relevant page.
|
|
| 1730 | 1717 |
|
| 1731 | 1718 |
Profiling specific parts of BuildStream with BST_PROFILE
|
| 1732 | 1719 |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| ... | ... | @@ -35,6 +35,8 @@ from .._exceptions import CASCacheError |
| 35 | 35 |
|
| 36 | 36 |
from .casremote import BlobNotFound, _CASBatchRead, _CASBatchUpdate
|
| 37 | 37 |
|
| 38 |
+_BUFFER_SIZE = 65536
|
|
| 39 |
+ |
|
| 38 | 40 |
|
| 39 | 41 |
# A CASCache manages a CAS repository as specified in the Remote Execution API.
|
| 40 | 42 |
#
|
| ... | ... | @@ -371,7 +373,7 @@ class CASCache(): |
| 371 | 373 |
with contextlib.ExitStack() as stack:
|
| 372 | 374 |
if path is not None and link_directly:
|
| 373 | 375 |
tmp = stack.enter_context(open(path, 'rb'))
|
| 374 |
- for chunk in iter(lambda: tmp.read(4096), b""):
|
|
| 376 |
+ for chunk in iter(lambda: tmp.read(_BUFFER_SIZE), b""):
|
|
| 375 | 377 |
h.update(chunk)
|
| 376 | 378 |
else:
|
| 377 | 379 |
tmp = stack.enter_context(utils._tempnamedfile(dir=self.tmpdir))
|
| ... | ... | @@ -380,7 +382,7 @@ class CASCache(): |
| 380 | 382 |
|
| 381 | 383 |
if path:
|
| 382 | 384 |
with open(path, 'rb') as f:
|
| 383 |
- for chunk in iter(lambda: f.read(4096), b""):
|
|
| 385 |
+ for chunk in iter(lambda: f.read(_BUFFER_SIZE), b""):
|
|
| 384 | 386 |
h.update(chunk)
|
| 385 | 387 |
tmp.write(chunk)
|
| 386 | 388 |
else:
|
| ... | ... | @@ -40,19 +40,34 @@ class Includes: |
| 40 | 40 |
includes = [_yaml.node_get(node, str, '(@)')]
|
| 41 | 41 |
else:
|
| 42 | 42 |
includes = _yaml.node_get(node, list, '(@)', default_value=None)
|
| 43 |
+ |
|
| 44 |
+ include_provenance = None
|
|
| 43 | 45 |
if '(@)' in node:
|
| 46 |
+ include_provenance = _yaml.node_get_provenance(node, key='(@)')
|
|
| 44 | 47 |
del node['(@)']
|
| 45 | 48 |
|
| 46 | 49 |
if includes:
|
| 47 | 50 |
for include in reversed(includes):
|
| 48 | 51 |
if only_local and ':' in include:
|
| 49 | 52 |
continue
|
| 50 |
- include_node, file_path, sub_loader = self._include_file(include,
|
|
| 51 |
- current_loader)
|
|
| 53 |
+ try:
|
|
| 54 |
+ include_node, file_path, sub_loader = self._include_file(include,
|
|
| 55 |
+ current_loader)
|
|
| 56 |
+ except LoadError as e:
|
|
| 57 |
+ if e.reason == LoadErrorReason.MISSING_FILE:
|
|
| 58 |
+ message = "{}: Include block references a file that could not be found: '{}'.".format(
|
|
| 59 |
+ include_provenance, include)
|
|
| 60 |
+ raise LoadError(LoadErrorReason.MISSING_FILE, message) from e
|
|
| 61 |
+ elif e.reason == LoadErrorReason.LOADING_DIRECTORY:
|
|
| 62 |
+ message = "{}: Include block references a directory instead of a file: '{}'.".format(
|
|
| 63 |
+ include_provenance, include)
|
|
| 64 |
+ raise LoadError(LoadErrorReason.LOADING_DIRECTORY, message) from e
|
|
| 65 |
+ else:
|
|
| 66 |
+ raise
|
|
| 67 |
+ |
|
| 52 | 68 |
if file_path in included:
|
| 53 |
- provenance = _yaml.node_get_provenance(node)
|
|
| 54 | 69 |
raise LoadError(LoadErrorReason.RECURSIVE_INCLUDE,
|
| 55 |
- "{}: trying to recursively include {}". format(provenance,
|
|
| 70 |
+ "{}: trying to recursively include {}". format(include_provenance,
|
|
| 56 | 71 |
file_path))
|
| 57 | 72 |
# Because the included node will be modified, we need
|
| 58 | 73 |
# to copy it so that we do not modify the toplevel
|
| ... | ... | @@ -101,7 +116,7 @@ class Includes: |
| 101 | 116 |
file_path = os.path.join(directory, include)
|
| 102 | 117 |
key = (current_loader, file_path)
|
| 103 | 118 |
if key not in self._loaded:
|
| 104 |
- self._loaded[key] = _yaml.load(os.path.join(directory, include),
|
|
| 119 |
+ self._loaded[key] = _yaml.load(file_path,
|
|
| 105 | 120 |
shortname=shortname,
|
| 106 | 121 |
project=project,
|
| 107 | 122 |
copy_tree=self._copy_tree)
|
| ... | ... | @@ -235,7 +235,7 @@ def sha256sum(filename): |
| 235 | 235 |
try:
|
| 236 | 236 |
h = hashlib.sha256()
|
| 237 | 237 |
with open(filename, "rb") as f:
|
| 238 |
- for chunk in iter(lambda: f.read(4096), b""):
|
|
| 238 |
+ for chunk in iter(lambda: f.read(65536), b""):
|
|
| 239 | 239 |
h.update(chunk)
|
| 240 | 240 |
|
| 241 | 241 |
except OSError as e:
|
| 1 | 1 |
import os
|
| 2 |
+import textwrap
|
|
| 2 | 3 |
import pytest
|
| 3 | 4 |
from buildstream import _yaml
|
| 4 | 5 |
from buildstream._exceptions import ErrorDomain, LoadErrorReason
|
| ... | ... | @@ -27,6 +28,46 @@ def test_include_project_file(cli, datafiles): |
| 27 | 28 |
assert loaded['included'] == 'True'
|
| 28 | 29 |
|
| 29 | 30 |
|
| 31 |
+def test_include_missing_file(cli, tmpdir):
|
|
| 32 |
+ tmpdir.join('project.conf').write('{"name": "test"}')
|
|
| 33 |
+ element = tmpdir.join('include_missing_file.bst')
|
|
| 34 |
+ |
|
| 35 |
+ # Normally we would use dicts and _yaml.dump to write such things, but here
|
|
| 36 |
+ # we want to be sure of a stable line and column number.
|
|
| 37 |
+ element.write(textwrap.dedent("""
|
|
| 38 |
+ kind: manual
|
|
| 39 |
+ |
|
| 40 |
+ "(@)":
|
|
| 41 |
+ - nosuch.yaml
|
|
| 42 |
+ """).strip())
|
|
| 43 |
+ |
|
| 44 |
+ result = cli.run(project=str(tmpdir), args=['show', str(element.basename)])
|
|
| 45 |
+ result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.MISSING_FILE)
|
|
| 46 |
+ # Make sure the root cause provenance is in the output.
|
|
| 47 |
+ assert 'line 4 column 2' in result.stderr
|
|
| 48 |
+ |
|
| 49 |
+ |
|
| 50 |
+def test_include_dir(cli, tmpdir):
|
|
| 51 |
+ tmpdir.join('project.conf').write('{"name": "test"}')
|
|
| 52 |
+ tmpdir.mkdir('subdir')
|
|
| 53 |
+ element = tmpdir.join('include_dir.bst')
|
|
| 54 |
+ |
|
| 55 |
+ # Normally we would use dicts and _yaml.dump to write such things, but here
|
|
| 56 |
+ # we want to be sure of a stable line and column number.
|
|
| 57 |
+ element.write(textwrap.dedent("""
|
|
| 58 |
+ kind: manual
|
|
| 59 |
+ |
|
| 60 |
+ "(@)":
|
|
| 61 |
+ - subdir/
|
|
| 62 |
+ """).strip())
|
|
| 63 |
+ |
|
| 64 |
+ result = cli.run(project=str(tmpdir), args=['show', str(element.basename)])
|
|
| 65 |
+ result.assert_main_error(
|
|
| 66 |
+ ErrorDomain.LOAD, LoadErrorReason.LOADING_DIRECTORY)
|
|
| 67 |
+ # Make sure the root cause provenance is in the output.
|
|
| 68 |
+ assert 'line 4 column 2' in result.stderr
|
|
| 69 |
+ |
|
| 70 |
+ |
|
| 30 | 71 |
@pytest.mark.datafiles(DATA_DIR)
|
| 31 | 72 |
def test_include_junction_file(cli, tmpdir, datafiles):
|
| 32 | 73 |
project = os.path.join(str(datafiles), 'junction')
|
| ... | ... | @@ -47,7 +88,7 @@ def test_include_junction_file(cli, tmpdir, datafiles): |
| 47 | 88 |
|
| 48 | 89 |
|
| 49 | 90 |
@pytest.mark.datafiles(DATA_DIR)
|
| 50 |
-def test_include_junction_options(cli, tmpdir, datafiles):
|
|
| 91 |
+def test_include_junction_options(cli, datafiles):
|
|
| 51 | 92 |
project = os.path.join(str(datafiles), 'options')
|
| 52 | 93 |
|
| 53 | 94 |
result = cli.run(project=project, args=[
|
| ... | ... | @@ -128,7 +169,7 @@ def test_junction_element_not_partial_project_file(cli, tmpdir, datafiles): |
| 128 | 169 |
|
| 129 | 170 |
|
| 130 | 171 |
@pytest.mark.datafiles(DATA_DIR)
|
| 131 |
-def test_include_element_overrides(cli, tmpdir, datafiles):
|
|
| 172 |
+def test_include_element_overrides(cli, datafiles):
|
|
| 132 | 173 |
project = os.path.join(str(datafiles), 'overrides')
|
| 133 | 174 |
|
| 134 | 175 |
result = cli.run(project=project, args=[
|
| ... | ... | @@ -143,7 +184,7 @@ def test_include_element_overrides(cli, tmpdir, datafiles): |
| 143 | 184 |
|
| 144 | 185 |
|
| 145 | 186 |
@pytest.mark.datafiles(DATA_DIR)
|
| 146 |
-def test_include_element_overrides_composition(cli, tmpdir, datafiles):
|
|
| 187 |
+def test_include_element_overrides_composition(cli, datafiles):
|
|
| 147 | 188 |
project = os.path.join(str(datafiles), 'overrides')
|
| 148 | 189 |
|
| 149 | 190 |
result = cli.run(project=project, args=[
|
| ... | ... | @@ -158,7 +199,7 @@ def test_include_element_overrides_composition(cli, tmpdir, datafiles): |
| 158 | 199 |
|
| 159 | 200 |
|
| 160 | 201 |
@pytest.mark.datafiles(DATA_DIR)
|
| 161 |
-def test_include_element_overrides_sub_include(cli, tmpdir, datafiles):
|
|
| 202 |
+def test_include_element_overrides_sub_include(cli, datafiles):
|
|
| 162 | 203 |
project = os.path.join(str(datafiles), 'sub-include')
|
| 163 | 204 |
|
| 164 | 205 |
result = cli.run(project=project, args=[
|
| ... | ... | @@ -192,7 +233,7 @@ def test_junction_do_not_use_included_overrides(cli, tmpdir, datafiles): |
| 192 | 233 |
|
| 193 | 234 |
|
| 194 | 235 |
@pytest.mark.datafiles(DATA_DIR)
|
| 195 |
-def test_conditional_in_fragment(cli, tmpdir, datafiles):
|
|
| 236 |
+def test_conditional_in_fragment(cli, datafiles):
|
|
| 196 | 237 |
project = os.path.join(str(datafiles), 'conditional')
|
| 197 | 238 |
|
| 198 | 239 |
result = cli.run(project=project, args=[
|
| ... | ... | @@ -222,7 +263,7 @@ def test_inner(cli, datafiles): |
| 222 | 263 |
|
| 223 | 264 |
|
| 224 | 265 |
@pytest.mark.datafiles(DATA_DIR)
|
| 225 |
-def test_recusive_include(cli, tmpdir, datafiles):
|
|
| 266 |
+def test_recursive_include(cli, datafiles):
|
|
| 226 | 267 |
project = os.path.join(str(datafiles), 'recursive')
|
| 227 | 268 |
|
| 228 | 269 |
result = cli.run(project=project, args=[
|
| ... | ... | @@ -231,6 +272,7 @@ def test_recusive_include(cli, tmpdir, datafiles): |
| 231 | 272 |
'--format', '%{vars}',
|
| 232 | 273 |
'element.bst'])
|
| 233 | 274 |
result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.RECURSIVE_INCLUDE)
|
| 275 |
+ assert 'line 2 column 2' in result.stderr
|
|
| 234 | 276 |
|
| 235 | 277 |
|
| 236 | 278 |
@pytest.mark.datafiles(DATA_DIR)
|
