-
Notifications
You must be signed in to change notification settings - Fork 235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add speedscope renderer #160
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @goxberry ! This could be a nice addition. I haven't had time for a full review, but here are some thoughts.
pyinstrument/renderers/speedscope.py
Outdated
def encode_speedscope_frame(sframe: SpeedscopeFrame) -> str: | ||
"""Returns a string encoding a SpeedscopeFrame as a JSON object.""" | ||
|
||
property_decls: list[str] = [] | ||
property_decls.append('"name": %s' % encode_str(sframe.name)) | ||
property_decls.append('"file": %s' % encode_str(sframe.file)) | ||
property_decls.append('"line": %d' % sframe.line) | ||
|
||
return "{%s}" % ",".join(property_decls) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The existing jsonmodule.py uses this structure to encode JSON due to limits on the depth of objects that the built-in json
module can't handle. This code would be neater just using json.dumps
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see there are a few other places where the code is building json as strings. The thing to try here is to add your renderer to the overflow test - test/test_overflow.py
- if that passes, and you can use the built-in json
module, that would be preferable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make the code neater, I have:
- added JSON encoders for
SpeedscopeEvent
andSpeedscopeFrame
inbeb07eac
and9d32a191
, respectively - replaced the
encode_speedscope_event
andencode_speedscope_frame
calls with calls tojson.dumps
in582925f5
anda78ee717
, respectively - deleted the
encode_speedscope_event
andencode_speedscope_frame
functions in7a4f2e8b
and4a9ccfd4
, respectively
So far, the overflow test passes. I think converting other parts of SpeedscopeRenderer
to use json.dumps
should be straightforward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As part of porting the rest of the SpeedscopeRenderer
implementation to use json.dumps
, I ended up replacing the NamedTuple
types with dataclasses
, because those are easier to serialize to JSON via the __dict__
field. This work should be done as of e66984e4
.
a6ede78
to
4a9ccfd
Compare
I think this PR is probably ready for a second review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Brilliant, this is looking really good, @goxberry! I appreciate the effort you've put into this :)
Would you be able to add the Speedscope renderer to the overflow test and write a simple test for the speedscope output?
I'm thinking something like:
- test starts profiler
- test calls function a
- function a calls function b
- function b sleeps (you can use 'fake time' for this, see other tests)
- function b returns
- function a returns
- function a calls function b
- test stops profiler
- test renders speedscope JSON
- test loads speedscope JSON
- test checks a few things about the JSON, I suppose, a few things about events and metadata. I'm not sure of the specifics, perhaps the events should be [open, open, close, close]?
@joerick Thanks! And thank you for your review and for writing this profiler! Your design has made it straightforward to add this new feature. As you've suggested, I've added As a result of testing, I noticed something peculiar in
I notice that the value of You should be able to reproduce this phenomenon yourself by running in a terminal: > pytest --trace ${PATH_TO_PYINSTRUMENT_REPO}/test/test_profiler.py -k "test_speedscope_output"
(Pdb) b 149
(Pdb) c
(Pdb) s # should step into speedscope.py.__init__() here
(Pdb) b 220
(Pdb) c
(Pdb) p session.duration # on my machine, this command prints the value 0.00041103363037109375
(Pdb) p self._event_time # on my machine, this command prints 0.75, as expected, within a tolerance of 0.3 where the first Do you have any insight as to why this behavior might occur? |
Hi @goxberry! Yes, I can offer an explanation. The session.duration is computed by calling pyinstrument/test/fake_time_util.py Lines 23 to 32 in 96dcb80
This monkeypatches If this is causing a problem, you could edit the |
Thanks for the explanation!
In the interest of pragmatism, let's ignore the difference. I'll revert the change in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @goxberry! Thanks for adding the test. I have a couple requests :)
This commit starts the implementation of a speedscope renderer by: * copying the existing JSONRenderer to a new SpeedscopeRenderer class in `speedscope.py` of the `renderers` directory * adding an import hook to `__init__.py` of the `renderers` directory * adding a `speedscope` option to the `-r` flag at the command line
This commit replaces the SpeedscopeRenderer implementation copied from JSONRenderer with an implementation of an "evented" speedscope JSON profile output file writer. The file format is documented in a few different ways, listed below: * the speedscope GitHub repo wiki: https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources * the speedscope JSON schema: https://www.speedscope.app/file-format-schema.json * a Typescript file documenting the schema https://github.com/jlfwong/speedscope/blob/master/src/lib/file-format-spec.ts * a simple example https://github.com/jlfwong/speedscope/blob/100578c536a3afab39fb6803d28913d12eac29c5/sample/profiles/speedscope/0.0.1/simple.speedscope.json The style of the implementation needs obvious work; this commit is intended as a starting point for a pull request with the expectation that upstream maintainer feedback will be needed to fix any style or documentation issues.
This commit deletes the jsonrenderer comment in the SpeedscopeRenderer implementation; the source file is obviously not named jsonrenderer.
This commit converts SpeedscopeFrame from a non-class-style namedtuple to a class-style namedtuple for readability.
This commit uncomments the SpeedscopeEventType enumeration class and uses it in the SpeedscopeRenderer implementation.
This commit converts the SpeedscopeEvent namedtuple from non-class-type to class-type to make it more self-documenting and to conform with project guidelines regarding type hints.
This commit revises the docstring for SpeedscopeRenderer.render_frame by deleting text that no longer applies to the implementation of this method.
This commit uses the previously-unused profile_name object within SpeedscopeRenderer.render.
This commit adds a JSON encoder class for the SpeedscopeEvent namedtuple in order to stand up a SpeedscopeRenderer implementation based on the Python json module.
This commit renames the _total_time field to _event_time in order to clarify its purpose in the SpeedscopeRenderer class.
This commit replaces calls of the encode_speedscope_frame function with json.dumps calls.
This commit deletes the encode_speedscope_event function because it is no longer needed, and has been replaced with calls to json.dumps and SpeedscopeEventEncoder.
This commit adds a JSON encoder for the SpeedscopeFrame class to try to stand up a SpeedscopeRenderer implementation that uses the json module.
This commit replaces calls of encode_frame with calls of json.dumps.
This commit deletes the encode_frame function because it is no longer used.
This commit adds type hints to the Speedscope-related JSON encoders.
This commit deletes the import of `collections.namedtuple` because the speedscope renderer now uses class-style namedtuple definitions.
This commit corrects the type hints by making each field a union type with None, because `pyright` does not detect that `frame` cannot be `None` where `SpeedscopeFrame.__init__` is invoked.
This commit removes type hints from `SpeedscopeEventType` in order to silence some `pyright` errors regarding incorrect types when using `str` as the type hint for each value.
This commit changes the SpeedscopeFrame and SpeedscopeEvent types from class-style namedtuples to dataclasses because I couldn't figure out how to serialize namedtuples to JSON objects via: * subclassing json.JSONEncoder * defining a default method Attempting to return a dictionary using the data in each namedtuple did not seem to yield a string containing a JSON object; instead, a string containing a JSON array was returned. Changing the namedtuple types to dataclasses and leveraging the __dict__ dunder field, in concert with subclassing json.JSONEncoder and defining a default method, yielded the desired result, although memory usage will increase slightly.
This commit adds a SpeedscopeProfile class that stores the data corresponding to speedscope "profile" objects, and adds a SpeedscopeProfileEncoder class to serialize that class to JSON. The encoder classes will be consolidated in a later commit.
This commit deletes the commented-out dead code used by the pure string approach to serializing speedscope profile objects.
This commit deletes an orphaned LIFO iteration order comment about dictionaries in Python 3.7+.
This commit adds a SpeedscopeFile data class and a SpeedscopeEncoder class to serialize to JSON the SpeedscopeFrame, SpeedscopeEvent, SpeedscopeProfile, SpeedscopeEventType, and SpeedscopeFile data classes.
This commit revises the SpeedscopeRenderer class by removing a lot of dead code and updating docstrings and comments in the class and its auxiliary classes.
This commit deletes the processor.aggregate_repeated_calls method from the list of default processors returned by SpeedscopeRenderer because speedscope is a timeline-based format, and aggregating repeated calls fouls up a timeline view.
This commit updates a code comment within SpeedscopeRenderer.render discussing why the frame list is constructed as it is.
This commit fixes some pyright errors in pyinstrument/renderers/speedscope.py.
This commit replaces the for loop that builds up the speedscope frame list with a list comprehension.
This commit adds SpeedscopeRenderer and the `-r speedscope` flag to the pyinstrument documentation.
This commit removes the dataclass arguments from the SpeedscopeEvent class because this class does not need to be hashable.
This commit modifies the display title of a speedscope profile exported from pyinstrument to include the timestamp of when the profile was generated (which also happens to be argument to `--load-prev` needed to render output in other formats).
This commit changes the SpeedscopeRenderer code to comply with project style guidelines regarding code formatting with black and isort.
This commit adds SpeedscopeRenderer to the overflow test in pyinstrument's test suite.
This commit fixes an apparent inconsistency in the profiles[0].endValue field of the Speedscope output from SpeedscopeRenderer. In unit testing, the value of session.duration (within a call to SpeedscopeRenderer.render) may not be equal to the event time of the last event in the profiles[0].events field of the Speedscope output. In fact, in the test_speedscope_output test within test_profiler.py, the value of session duration was approximately 0.0047s (in local testing on my laptop), whereas the time value of the last event generated by the profile in that test should be 0.75s +/- 0.3s. To correct this inconsistency, the end value of profile is set equal to the event time of the last event.
This commit adds a test to test_profiler that tests the JSON output emitted by SpeedscopeRenderer against known properties it should have.
This commit: * Removes, in `test/test_profiler.py`, the test of the value of the `"profiles[0].endValue"` field in the Speedscope JSON output. The difference between the value of `profiles[0].endValue` (equal to `session.duration` from the `session` argument passed to `SpeedscopeRenderer.render`) and the time of the last event can be attributed to the `fake_time` context manager used as a mock timer in the profiler/renderer tests. * Changes the value assigned to the `profiles[0].endValue` field via the last positional argument to `SpeedscopeProfile.__init__` from `self._event_time` (which, at that point in `SpeedscopeRenderer.render` equals the last event time) back to `session.duration`. This change is made because the discrepancy between the values of `session.duration` and `self._event_time` can be attributed to the `fake_time` context manager used as a mock timer in the profiler/renderer tests.
This commit aims to make the `test_speedscope_output` test in `test/test_profiler.py` less wordy and more readable by: * deleting message arguments to assertions * replacing local variables with literals or expressions, because many of these variables were motivated by keeping line length low in message arguments passed to assertions * removing tolerances in the pytest.approx calls because CI jitter should not affect timings, and all floating point numbers involved are exactly representable per the IEEE-754 standard
97925fb
to
99dd85f
Compare
Rebased on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
Whoah :O Thank you guys so much!! This is so cool!! :D |
This pull request adds a renderer that outputs JSON conforming to speedscope's JSON schema; in particular, speedscope's "evented" format is used.
This implementation is very rough, and intended as a request for comment. I expect to revise this pull request based on feedback from the project maintainer(s) to better conform with project style.
Closes #89.