-
Notifications
You must be signed in to change notification settings - Fork 622
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
implement diff rendering #289
Conversation
Codecov Report
@@ Coverage Diff @@
## main #289 +/- ##
==========================================
+ Coverage 53.91% 54.66% +0.75%
==========================================
Files 100 101 +1
Lines 4426 4622 +196
==========================================
+ Hits 2386 2526 +140
- Misses 1811 1866 +55
- Partials 229 230 +1
Continue to review full report at Codecov.
|
c400afe
to
638c239
Compare
7d13c67
to
94dd1a9
Compare
pkg/server/render.go
Outdated
var err error | ||
var out, leftOut, rghtOut *storage.GetOutput | ||
|
||
leftStartTime, leftEndTime, leftOK := parseRenderRangeParams(r.URL.Query(), "leftFrom", "leftUntil") | ||
rghtStartTime, rghtEndTime, rghtOK := parseRenderRangeParams(r.URL.Query(), "rightFrom", "rightUntil") | ||
|
||
isFormatDouble := rghtOK || leftOK | ||
if isFormatDouble { |
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.
It may be way more convenient to introduce a new endpoint. Let's say, render-diff
(or query-diff
), that would accept POST
with the following payload:
type RenderDiffParams struct {
Format string
MaxNodes int
Left, Right RenderTreeParams
}
type RenderTreeParams struct {
From string
To string
Query *string
Name *string
}
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 generally follow the convention from the original code, where /render
is GET
with from=...&until=...
.
Because out
is required, hence from
and until
are required. So extending the query with leftFrom
, leftUntil
, rightFrom
, rightUntil
is a reasonable choice.
(personally, I prefer using POST
in API, but I followed the project convention here)
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 think we define conventions ourselves :)
I believe that every endpoint/handler should solve exactly one problem, therefore I'd introduce a new one for rending diff. And POST with JSON-encoded parameters would be the most appropriate here, IMO.
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'm ok with that. Let's add @petethepig to this conversation so we can have another opinion.
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.
@olvrng I generally agree with @kolesnikovae — I think it would make the API simpler if we split it. But I know with these kinds of things there might be other non-obvious reasons to doing things in one function, so I would like to talk to you about this before we change it.
pkg/server/render.go
Outdated
|
||
isFormatDouble := rghtOK || leftOK | ||
if isFormatDouble { | ||
out, leftOut, rghtOut, err = ctrl.loadTreeConcurrently(p.gi, p.gi.StartTime, p.gi.EndTime, leftStartTime, leftEndTime, rghtStartTime, rghtEndTime) |
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.
Not quite sure what we need this out
for.
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.
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.
If we don't use the tree from out
, perhaps it worth extending storage
with a method for timeline retrieval (without the resulting tree).
Also I'm wondering how we should handle diff/comparison with respect to queries/tags.
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.
Maybe we should add another PR for extending storage
and queries/tags.
pkg/server/render.go
Outdated
defer func() { | ||
rerr := recover() | ||
if rerr != nil { | ||
_err = fmt.Errorf("panic: %v", rerr) | ||
} | ||
}() |
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.
Consider implementing recoveryMiddleware
that would wrap all handlers and catch all panics.
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.
Actually, this method is intended to be called in loadTreeConcurrently
:
var treeErr, leftErr, rghtErr error
var wg sync.WaitGroup
wg.Add(3)
go func() { defer wg.Done(); treeOut, treeErr = ctrl.loadTree(gi, treeStartTime, treeEndTime) }()
go func() { defer wg.Done(); leftOut, leftErr = ctrl.loadTree(gi, leftStartTime, leftEndTime) }()
go func() { defer wg.Done(); rghtOut, rghtErr = ctrl.loadTree(gi, rghtStartTime, rghtEndTime) }()
wg.Wait()
So middleware is not an option here. We must catch all panics in each goroutine or the whole process will stop.
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.
recoveryMiddleware
is not supposed to be a replacement for this recovery. There are many other places in handlers routines that may cause panic.
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'm not sure I understand your last comment. Should I keep that code or use something else?
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.
If you are concerned with possible panics, in addition to this panic recovery (which scope is loadTree
) consider implementing recovery middleware like this: https://github.com/pyroscope-io/pyroscope/blob/3bca77ab0930ef02021719b463a2009a31085450/pkg/server/controller.go#L303
that would catch all panics in all handlers (but, of course, not in goroutines created by them). Although a panic in an HTTP handler does not cause app crash (directly), recovering improves user experience, especially if server logs are collected and parsed.
Regarding panic recovery in general: it may make sense to capture stack trace, otherwise error message may be not informative.
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.
No, I mean the code is intended to be used in goroutines, not in an HTTP handler:
func (ctrl *Controller) loadTreeConcurrently(...) {
var treeErr, leftErr, rghtErr error
var wg sync.WaitGroup
wg.Add(3)
// call here ⤵ in a new goroutine
go func() { defer wg.Done(); treeOut, treeErr = ctrl.loadTree(gi, treeStartTime, treeEndTime) }()
go func() { defer wg.Done(); leftOut, leftErr = ctrl.loadTree(gi, leftStartTime, leftEndTime) }()
go func() { defer wg.Done(); rghtOut, rghtErr = ctrl.loadTree(gi, rghtStartTime, rghtEndTime) }()
wg.Wait()
// ...
}
func (ctrl *Controller) loadTree(...) {
defer func() {
rerr := recover() // <- capture here
// ...
}()
// ...
}
We should always capture panic inside a goroutine, or the process will crash. This is different from HTTP handlers.
(for HTTP handlers, the project should already have recover middleware somewhere, it's out of scope for this PR)
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.
Okay, thanks @olvrng.
Meanwhile, please take into account:
Regarding panic recovery in general: it may make sense to capture stack trace, otherwise error message may be not informative.
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.
Yes, I see. A stack trace log should be good enough for now. Is it ok?
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.
Yep, debug.Stack()
should tell us everything.
I have a suggestion to merge it with #290. What is the benefit of having two separate branches? |
I generally split server and client implementation. As described the commit tree:
The client implementation (
|
@olvrng I closed the other 2 prs to reduce confusion and consolidate the discussion to just one PR. Feel free to copy the todos from the other PRs. |
d7a81d9
to
0a8f5af
Compare
0a8f5af
to
6b7337d
Compare
51e9d26
to
3719757
Compare
3719757
to
dc39e85
Compare
d508400
to
42f52b1
Compare
</td> | ||
{/* NOTE: it seems React does not understand multiple backgrounds, have to workaround: */} | ||
{/* The `style` prop expects a mapping from style properties to values, not a string. */} | ||
<td STYLE={backgroundImageDiffStyle(x.selfLeft, x.selfRght, maxSelf, color, 'L')}> |
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.
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 try to use multiple backgrounds here. The original code that I tested (and it didn't work) was:
<!-- tried "style" instead of "STYLE" -->
<td style={backgroundImageDiffStyleALT(x.selfLeft, x.selfRght, maxSelf, color, 'L')}>
function backgroundImageDiffStyleALT(a, b, total, color, side) {
// ...
// tried object instead of string for css
return {
backgroundImage: `linear-gradient(${clr}, ${clr}), linear-gradient(${cld}, ${cld})`;
backgroundPosition: `${neg(-k)}px 0px, ${neg(-kd)}px 0px`;
backgroundRepeat: `no-repeat`;
};
}
It didn't work. And I had to change to the current code:
<!-- workaround: use "STYLE" -->
<td STYLE={backgroundImageDiffStyle(x.selfLeft, x.selfRght, maxSelf, color, 'L')}>
function backgroundImageDiffStyle(a, b, total, color, side) {
// ...
// workaround: use string for css
return `
background-image: linear-gradient(${clr}, ${clr}), linear-gradient(${cld}, ${cld});
background-position: ${neg(-k)}px 0px, ${neg(-kd)}px 0px;
background-repeat: no-repeat;
`;
}
If you can make the original code work or find a better workaround, I'm happy to learn about it.
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 fixed the first warning Warning: Each child in a list should have a unique "key" prop.
. My mistake.
level[i] += prev; | ||
prev = level[i] + level[i + 1]; | ||
} | ||
} | ||
} | ||
|
||
export function deltaDiffWrapper(format, levels) { | ||
if (format === "double") { |
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.
Can you explain why there are two deltaDiff
here with different start / steps with one happening right before the other?
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 corresponding code on the server is here: diff
func CombineToFlamebearerStruct(leftTree, rightTree *Tree, maxNodes int) *Flamebearer {
// ...
// delta encoding
deltaEncoding(res.Levels, 0, 7)
deltaEncoding(res.Levels, 3, 7)
return &res
}
With format=double
, the response from the server combines values from 2 left and right trees into a single array for levels
, start at i+0
and i+3
with step=7
:
i+0 = x offset, left tree
i+1 = total , left tree
i+2 = self , left tree
i+3 = x offset, right tree
i+4 = total , right tree
i+5 = self , right tree
i+6 = index in the names array
The levels
array is encoded using delta encoding for x offset on the left and the right trees before being sent to the client. Therefore, the client needs to decode it before rendering.
42f52b1
to
73b52ab
Compare
Looks great thanks @olvrng! After merging this I'll follow up with creating some new issues for some of the todo's / future improvements needed here. Feel free to do the same for anything you feel like you didn't get to on this PR but is worth addressing in the future |
This PR implements diff rendering:
single | double
/render
and/render-diff
Depends: #288