Skip to content
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

Summary callback changes #1768

Merged
merged 4 commits into from
Jan 13, 2021
Merged

Summary callback changes #1768

merged 4 commits into from
Jan 13, 2021

Conversation

na--
Copy link
Member

@na-- na-- commented Dec 14, 2020

This is a WIP attempt at fixing a bunch of issues (fixes #1316, fixes #1611, closes #817, closes #647, closes #351) and making possible the implementation of a bunch of other ones (#1315, #1675, #1319)...

The way we do that is by supporting a new handleSummary() exported function that allows users to customize the test summary (or summaries) however the want. The final goal is something like this:

import http from 'k6/http';
import { JUnit, TextSummary } from 'https://jslib.k6.io/k6-summary/1.0.0/index.js' // or something like that
// ...
export function handleSummary(data) {
    // save everything to files and send it to a server when the test finishes or gets aborted
    http.post('https://example.com/testResults', JSON.stringify(data));

    return {
        'stdout': TextSummary(data),
        './results/junit.xml': JUnit(data),
        './results/summary.json': JSON.stringify(data),
    }
}

With the current state of the PR, handleSummary() is supported and its results are saved to stderr/stdout/files. I've also heavily refactored the --summary-export (implemented it through the new system in a backwards compatible way) and fixed a bunch of --sumary-trend-stats bugs and group/check order. But we don't have a JS equivalent of the old summary (TextSummary() in the example above) yet or JUnit(). For now, I've exposed the old Go code that generated the test summary as a second parameter to handleSummary(), but I'll remove that in the final version. So, to test this, you might use something like this:

export function handleSummary(data, oldCallback) {
    console.log('handleSummary() was called');
    return {
        'stdout': oldCallback(),
        'raw-data.json': JSON.stringify(data, null, 4),
    };
}

The following things remain to be done:

Breaking changes and other bugfixes

  • --no-summary now disables even --summary-export
  • the value for Rate metrics in the old --summary-export JSON file was was always 0, regardless of the pass/(pass+fail) ratio

@na-- na-- changed the title [WIP [WIP] Summary callback changes Dec 14, 2020
@na--
Copy link
Member Author

na-- commented Dec 14, 2020

Here is a more complete example that also demonstrates the fixed summary columns even in the old backwards-compatible --summary-export handling, and the fact that the wrappers can work in --compatibility-mode=base: https://gist.github.com/na--/647cbafe46abd1a0af37402577e5d251

@sniku
Copy link
Collaborator

sniku commented Dec 16, 2020

I'm working on creating an end-of-test summary for functional/integration tests.
This will take some time, so I'll just post comments here as I work through it. I don't expect any comments back since we have holidays! 😄

The example posted above is hitting a bug/mistake in the summaryWrapperLamnbdaCode. The result is:

ERRO[0001] handleSummary() failed with error "TypeError: Value is not an object: undefined", falling back to the default summary  source=console

oldCallback should probably be passed to exportedSummaryCallback as a second argument.

https://github.com/loadimpact/k6/pull/1768/files#diff-a0d09d808d4fa4f068a2bb81b9fa180f596f46439d040f840687d559404b5cd9R99

I can fix/work around it, so it's not a problem.

Copy link
Contributor

@imiric imiric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! 👏

The code LGTM, besides the minor comments. I'll take another look next year ;)

Just some notes:

  • Strong 👍 for splitting this into separate commits/PRs. It would've been easier to review like that as well. 😅
  • I like the GetResolversForTrendColumns() refactor, despite of those Sink pain points. 👍

js/runner.go Show resolved Hide resolved
stats/stats.go Outdated Show resolved Hide resolved
stats/stats.go Outdated Show resolved Hide resolved
stats/stats.go Outdated Show resolved Hide resolved
todo.txt Outdated Show resolved Hide resolved
cmd/runtime_options.go Outdated Show resolved Hide resolved
js/runner.go Outdated Show resolved Hide resolved
@mstoykov
Copy link
Contributor

mstoykov commented Jan 4, 2021

I have 2 concept/UX/big issues:

  1. I think I've already expressed this - but I don't think returning an object is good, IMO it's probably better to provide some API, through one of the arguments, not an import, on how to write to stdout/files so something like:
import http from 'k6/http';
import { JUnit, TextSummary } from 'https://jslib.k6.io/k6-summary/1.0.0/index.js' // or something like that
// ...
export function handleSummary(data, api)  { // Worst name ever I know :)
    // save everything to files and send it to a server when the test finishes or gets aborted
    http.post('https://example.com/testResults', JSON.stringify(data));
    api.writeToStdout(TextSummary(data));
    // or TextSummary(api.stdout, data);
    var f = api.openFile("./results/junit.xml");
    f.write(JUnit(data));
    // or JUnit(f, data); 
}

This will (without generators that are currently not supported, last I checked) let us write big responses without being memory hungry.
All of those objects/functions should have very similar syntax - probably just write which appends data, we can add seek to the file ones at a later point if there is sufficient demand.

I haven't looked into it, but I expect there is a somewhat common "interface" for this in the JS world that can be used.

  1. I feel like there will be a lot of cases where the test will be executed and the function will throw an exception because the test wasn't called correctly and the most common case IMO will be because the handleSummary will expect that it has p(23) but the user didn't' call it with the correct --summary-trend-stats, so it will blow up after an hour of testing.

I think it is worth trying to figure out if we can actually have a way that this is prevented. My first thought (which works great for the summary-trend-stats) is to completely change the API to something like (all names of stuff are bad and only for illustrative purposes):

import summary from "k6/summary";

summary.register(function(data) {<same as previously>}, ["p(23)"]);

Where the first argument is the function as before and the second is something that says which summaryTrendStats it wants.
This probably should be extended with other things such as ... if the user wants the data for http_req_duration with tag something=else . Especially this might be better just to be ... designed/agreed upon how it will work, but to be implemented in another PR.

edit; this second syntax also makes it easy to have multiple handleSummary functions making the different types instead of having a single one which makes all.

@na--
Copy link
Member Author

na-- commented Jan 4, 2021

@mstoykov, I think we've previously discussed most of these ideas in the private issue, but to rehash things:

I don't think returning an object is good, IMO it's probably better to provide some API, through one of the arguments, not an import, on how to write to stdout/files

If you recall, having some sort of a file writing API was port of my original proposal for this functionality. I later retracted this, because that API would have been a deep and bikesheddy rabbit hole which didn't (and still doesn't) seem worth getting into. I didn't want to have to figure out a nice JS API for file opening/closing, permissions, appending, binary data, etc. This feature is big enough without having to deal with that from scratch as well...

The currently returned {filepath: contents} map is basically a simple MVP version that would likely satisfy 95+% of users. Memory usage is unlikely to be a concern, even if someone decides to generate fancy PDF or HTML reports. And if this ever proves insufficient, we can always add a file writing API at a later point, as a separate PR. The current implementation is 20 lines of simple and testable code, so it won't be a big overhead to maintain.

Besides, any file writing API will probably be better off as something generic, and better of being polished as an xk6 extension for a while, not directly in core.

cc @sniku @imiric what are your thoughts here? I am not opposed to a proper API, but that would mean delaying the feature again and not getting it in v0.30 for sure.

I feel like there will be a lot of cases where the test will be executed and the function will throw an exception because the test wasn't called correctly...

True, that's a risk, but you can say the same thing about teardown(). If you saw, I plan to add some exception handling for errors in the JS handleSummary() wrapper, so we will at least show the default summary if an error is thrown by user code.

I don't think the UX of passing the required percentiles (["p(23)"]) and other data is very good. Even the toy example looks strange, but a real-life example would be even more awkward. And having percentile APIs instead of plain data wouldn't necessarily be less error prone.

That said, I like the approach of relying on an imported module and calling a method on it (summary.register()) more than relying on exported magic functions with specific names... I am not sure it makes sense to register() multiple handlers for the end-of-test summary, though we might want to instead make this a more generic event-handling system. Something like:

import events from "k6/events";

events.registerHandler('endOfTestSummary', someHandler); // or maybe addEventListener :D 
events.registerHandler('midTestSummary', someOtherHandler);
events.registerHandler('vuInit', perVUInit); 
events.registerHandler('abortedByThreshold', whatever); 
// ...

But again, this seems like it can grow in complexity and needs careful design because of the k6 multi-JS-runtime arhitecture. It also seems like something that can be added in the future, in a backwards compatible way. So, for now, starting with an exported handleSummary function doesn't seem like a bad idea and doesn't seem like it will introduce any overhead. If anything, most of the overhead of this feature comes from the huge amount of old text summary and --summary-export cruft. Once we wrap those in handleSummary(), it should be reasonably easy to work with in the future.

@imiric
Copy link
Contributor

imiric commented Jan 12, 2021

I like the flexibility of @mstoykov's proposal, but also feel that it would cause a delay of v0.30.0 to implement it. Since we can add a proper API in a backwards compatible way in the future (new users simply won't have to return an object), my vote is to merge this as is and discuss and design the API proposal in a subsequent issue/PR.

I also like the event-based registration idea for summary handlers, but that also feels out of scope for this MVP.

@na-- na-- added this to the v0.30.0 milestone Jan 12, 2021
@na-- na-- added the breaking change for PRs that need to be mentioned in the breaking changes section of the release notes label Jan 12, 2021
@codecov-io
Copy link

codecov-io commented Jan 12, 2021

Codecov Report

Merging #1768 (0838813) into master (def5ac7) will increase coverage by 0.00%.
The diff coverage is 69.30%.

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #1768   +/-   ##
=======================================
  Coverage   71.49%   71.50%           
=======================================
  Files         180      181    +1     
  Lines       13851    13939   +88     
=======================================
+ Hits         9903     9967   +64     
- Misses       3325     3340   +15     
- Partials      623      632    +9     
Flag Coverage Δ
ubuntu 71.48% <69.30%> (+0.03%) ⬆️
windows 70.12% <69.30%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
cmd/options.go 60.46% <0.00%> (ø)
cmd/run.go 7.29% <0.00%> (-0.10%) ⬇️
lib/testutils/minirunner/minirunner.go 80.43% <0.00%> (-0.97%) ⬇️
stats/sink.go 100.00% <ø> (ø)
js/common/util.go 46.66% <27.27%> (-53.34%) ⬇️
js/runner.go 80.86% <68.08%> (+0.41%) ⬆️
js/summary.go 90.00% <90.00%> (ø)
cmd/config.go 88.28% <100.00%> (+0.10%) ⬆️
stats/stats.go 80.63% <100.00%> (+3.51%) ⬆️
ui/summary.go 86.39% <100.00%> (-2.96%) ⬇️
... and 4 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update def5ac7...0838813. Read the comment docs.

Base automatically changed from misc-fixes-for-summary to master January 12, 2021 13:21
@na-- na-- requested review from imiric and mstoykov January 12, 2021 13:27
@na-- na-- changed the title [WIP] Summary callback changes Summary callback changes Jan 12, 2021
@mstoykov
Copy link
Contributor

I tried (successfully I would say) port https://github.com/Mattihew/k6-to-junit to this new change here

if err := s.SummarizeMetricsJSON(f, data); err != nil {
logger.WithError(err).Error("failed to make summary export file")
}
logger.WithError(err).Error("failed to handle the end-of-test summary")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that in a sequential release we will fallback to a default representation or the dumping the lib.Summary as JSON to the disk?

Copy link
Member Author

@na-- na-- Jan 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what you mean by "a sequential release", but we'll get an error here only if something goes majorly wrong... Both initRunner.HandleSummary() and handleSummaryResult() try to continue working even if there are errors. handleSummaryResult() tries to save all files, even if some of them fail (because of wrong names, lack of permissions, etc.). And initRunner.HandleSummary() has internal try / catch in the JS wrapper code: https://github.com/loadimpact/k6/blob/0838813db6edead0f6faa3cdaadf3b5eb070a1ab/js/summary.go#L97-L104

Though I should add tests for both of these things...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I missed this code at the time ...

This is probably my biggest issue reviewing the PR, the code is all over the place, although I don't know how to fix that if it's at all possible given what the code tries to do, so I don't have some concrete feedback. I hope that when we rewrite even more of the whole metric and threshold (which specifically are also over the place) this might get more streamlined

switch v := resolver(sink).(type) {
case float64:
v := resolver(sink)
if tc != "count" { // sigh
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick ... but why are you checking for the reverse here instead of for the equal one? ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️ probably just because that was the case order in the old switch, but yeah, doesn't makes sense, so I'll change it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the JS variant of this though, no need to further mess with the Go code

mstoykov
mstoykov previously approved these changes Jan 12, 2021
Copy link
Contributor

@mstoykov mstoykov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

I had some more comments, but even the runPart thing seems likely to be more involved with little actual benefit, so I guess it can be even be left for a later PR or not done at all.

js/runner.go Outdated
fn := exported.Get(consts.HandleSummaryFn)
if _, ok := goja.AssertFunction(fn); ok {
handleSummaryFn = fn
} else if fn != nil && !goja.IsUndefined(fn) && !goja.IsNull(fn) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked and Get returns nil if it doesn't exist. Did you intend for exports.handleSummary = undefined; (or null) to be a valid thing that won't raise an error or were you over zealous in the checks?

I am not particularly certain it matters either way, to be honest. If someone has defined handleSummary it is probably better to use delete exports.handleSummary, on the other hand, I don't think we will be hitting it. So the question is mostly questing what was the idea behind it, not changing it one way or another

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

were you over zealous in the checks?

Likely that, and it makes sense to leave only the nil check 👍

}
rawResult, _, _, err := vu.runFn(ctx, false, handleSummaryWrapper, wrapperArgs...)

// TODO: refactor the whole JS runner to avoid copy-pasting these complicated bits...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty sure you can just use runPart for most of the above.
You will need to keep the check for whether exports.handleSummary is a function, but everything else seems to be done by runPart either way ... I think ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I guess you will need to move https://github.com/loadimpact/k6/blob/0838813db6edead0f6faa3cdaadf3b5eb070a1ab/js/summary.go#L95-L118 to golang code after the runPart

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I initially tried using runPart, but it was an even bigger mess. We can figure out some way to refactor the whole js.Runner internally in the future to reduce the boilerplate, but for now I don't want to mess with it more if I don't need to... 😅

imiric
imiric previously approved these changes Jan 12, 2021
Copy link
Contributor

@imiric imiric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, nice work!

js/runner.go Show resolved Hide resolved
@na-- na-- dismissed stale reviews from imiric and mstoykov via 839b0b2 January 13, 2021 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change for PRs that need to be mentioned in the breaking changes section of the release notes
Projects
None yet
5 participants