Track and display middleware time in the toolbar#2049
Track and display middleware time in the toolbar#2049earthcomfy wants to merge 8 commits intodjango-commons:mainfrom
Conversation
|
Would it make sense to include a stat with the middleware time deducted from the total for the user? Or deduct it from the existing total? |
|
@robhudson the thinking I had earlier was that the toolbar changes the application so we shouldn't try to extract the toolbar itself from the timing. |
| # regardless of the response. Keep 'return' clauses below. | ||
| for panel in reversed(toolbar.enabled_panels): | ||
| panel.disable_instrumentation() | ||
| toolbar_end_time = perf_counter() |
There was a problem hiding this comment.
The other thing I was thinking about is how to handle measuring the full time of the toolbar's processing. This is actually stopping before all the Panel.generate_stats() methods are called which contains quite a bit of logic. I think ideally we'd capture that in this timing.
The problem is, how do we capture that? I have a few ideas, but I'm not happy with them.
I think we could measure the end time in TimerPanel.generate_stats() like we do with the rest and store it. This means any other panels that generate their stats after the TimerPanel would not be included. To mitigate that, we could find a way to make sure that TimerPanel is first to enable and last to generate stats. I think having it be first in enabled_panels would do the trick.
The other idea I had was to create a templatetag that would do the diff between the stored time and the current time at the time the toolbar is rendered. The problem here is that it wouldn't be compatible with the serializable branch where we want to store all the stats before rendering.
So I think trying to consolidate all the logic into TimerPanel with a change to force TimerPanel to be first for processing (not appearance on the idea) is ideal.
@salty-ivy @elineda any thoughts?
There was a problem hiding this comment.
I have updated the enabled_panels property to ensure the TimerPanel is first in order. This is I think what you meant by in the first approach. The order of enabled panels is reversed in _postprocess ensuring the end time is captured at the end.
There was a problem hiding this comment.
Excellent, thank you @earthcomfy! This is looking really nice. The last bit we need are the tests.
There was a problem hiding this comment.
I added a test to ensure the timer panel is first. Are there any other tests that need to be included
There was a problem hiding this comment.
@earthcomfy ideally we'd have tests covering all the changes on the Timer panel. Or updates to similar tests to also cover these new fields.
| if timer_panel: | ||
| panels.remove(timer_panel) | ||
| panels.insert(0, timer_panel) | ||
| return panels |
There was a problem hiding this comment.
@earthcomfy I'm going to need to take a bit to process this. This is a bit of a change and I want to make sure I understand it well. For example, I think this impacts the order of the panels on the side which means it impacts the UX of the toolbar.
Currently by default the Timer panel is 3rd from the top, while the top is the History panel. I think the History panel being at the top is between mildly important and important.
@django-commons/django-debug-toolbar any thoughts here?
There was a problem hiding this comment.
Yes, I agree that the history panel should stay at the top because it affects basically everything show below.
Maybe we could unconditionally (without checking if the timer panel is even used) save the start time of the toolbar in the middleware or in the DebugToolbar instance. The timer panel could then use self.toolbar or something to access this timing information if it's enabled.
There was a problem hiding this comment.
I've been going back and forth on that. I was hoping we could contain this entirely within the TimerPanel, but that may be more problematic than we'd like.
There was a problem hiding this comment.
Yes, it would be great if there was a way to keep it contained. But if the price we pay for that is that we either have to introduce a pre-init API or some way of ordering panels processing (priority? weight?) then I'd slightly prefer to keep the amount of code required down while uglifying things a bit. This isn't a strongly held conviction though.
There was a problem hiding this comment.
Yeah, that adds a fair amount of complexity. I'm leaning towards agreeing with you. @earthcomfy what do you think?
There was a problem hiding this comment.
The order on the ux is not affected by these changes isn't ?
The display order is calculated by the DebugToolbar.panels (debug_toolbar/toolbar.py:33), not DebugToolbar.enabled_panels (debug_toolbar/toolbar.py:62)
There was a problem hiding this comment.
I believe it is. They both end up coming from settings.get_panels()
There was a problem hiding this comment.
But the change isn't in the settings, it's ont the panel enabled.
|
@earthcomfy do you have time to pick this back up? If not, @robhudson could ask a Djangonaut from the next session of Djangonaut Space to get it over the finish line. |
|
@tim-schilling Thanks for following up! I don’t think I’ll have time to return to this, so feel free to reassign it:) |
|
Hey @tim-schilling, I'd like to pick this back up if that's okay. I checked this again, and I believe what @elineda pointed out is correct. I have verified manually as well that the change doesn't affect the order of the panels. The template (base.html) iterates over That said, if the team still prefers the other approach of storing timing on the DebugToolbar instance directly, I'm happy to go that route instead. PS: fixed the conflict and updated my branch to the latest. |
|
Awesome, glad to hear you can work on it again! I've got this on my list of things to review. I don't have a timetable though. |
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||
codingjoe
left a comment
There was a problem hiding this comment.
Hi @earthcomfy, I invested some time to give this a thorough code review. Please tell me if you want me to clarify some of my comments. Cheers! Joe
| # stats['usrss'] = self._end_rusage.ru_isrss | ||
|
|
||
| if hasattr(self, "_toolbar_start_time"): | ||
| stats["toolbar_time"] = (perf_counter() - self._toolbar_start_time) * 1000 |
There was a problem hiding this comment.
Consider storing the duration with an explicit type using datetime.timedelta. It's always easier than remembering that toolbar_time is in milliseconds.
| # stats['urss'] = self._end_rusage.ru_idrss | ||
| # stats['usrss'] = self._end_rusage.ru_isrss | ||
|
|
||
| if hasattr(self, "_toolbar_start_time"): |
There was a problem hiding this comment.
EAFP: Maybe consider catching the AttributeError instead of the default if-branch.
| def enable_instrumentation(self): | ||
| self._toolbar_start_time = perf_counter() | ||
|
|
||
| async def aenable_instrumentation(self): | ||
| self._toolbar_start_time = perf_counter() |
There was a problem hiding this comment.
Both are synchronous, and instrumentation usually lives in the CPU and has no IO. Thus, I don't think asyncIO is all that helpful even in the future.
| timer_panel = next( | ||
| (panel for panel in panels if panel.panel_id == "TimerPanel"), None | ||
| ) | ||
| if timer_panel: |
There was a problem hiding this comment.
Great place for a walrus:
| timer_panel = next( | |
| (panel for panel in panels if panel.panel_id == "TimerPanel"), None | |
| ) | |
| if timer_panel: | |
| if timer_panel := next( | |
| (panel for panel in panels if panel.panel_id == "TimerPanel"), None | |
| ) |
| panels.remove(timer_panel) | ||
| panels.insert(0, timer_panel) |
There was a problem hiding this comment.
Maybe replace the item instead of the stack operation.
| panels.remove(timer_panel) | |
| panels.insert(0, timer_panel) | |
| panels[0] = timer_panel |
| panels = [panel for panel in self._panels.values() if panel.enabled] | ||
| # Ensure TimerPanel is first in order to measure the full time of the toolbar's processing. | ||
| timer_panel = next( | ||
| (panel for panel in panels if panel.panel_id == "TimerPanel"), None | ||
| ) |
There was a problem hiding this comment.
You are looping over the panels twice, O(n)=2*n. I think with a little rearrangement, this can be done in a single loop.
| r'TimerPanel_stime;dur=(\d)*(\.(\d)*)?;desc="System CPU time", ', | ||
| r'TimerPanel_total;dur=(\d)*(\.(\d)*)?;desc="Total CPU time", ', | ||
| r'TimerPanel_total_time;dur=(\d)*(\.(\d)*)?;desc="Elapsed time", ', | ||
| r'TimerPanel_toolbar_time;dur=(\d)*(\.(\d)*)?;desc="Toolbar time", ', |
There was a problem hiding this comment.
Toolbar is ambiguous outside this repo. Maybe stick with DebugToolbar, like it's currently phrased in most places.
| r'TimerPanel_toolbar_time;dur=(\d)*(\.(\d)*)?;desc="Toolbar time", ', | |
| r'TimerPanel_dj_toolbar_time;dur=(\d)*(\.(\d)*)?;desc="DebugToolbar time", ', |
| (_("Total CPU time"), _("%(total)0.3f ms") % stats), | ||
| (_("Elapsed time"), _("%(total_time)0.3f ms") % stats), | ||
| ( | ||
| _("Toolbar time"), |
There was a problem hiding this comment.
Toolbar is ambiguous outside this repo. Maybe stick with DebugToolbar, like it's currently phrased in most places.
| _("Toolbar time"), | |
| _("DebugToolbar time"), |
Description
CPU time includes the debug toolbar's own time so this PR aims to track how long the middleware takes to run and display that time on the panel.
Fixes #1903
Checklist:
docs/changes.rst.