Version 0.53.0 release notes
k6 v0.53.0
is here 🎉! This release includes:
- Native ECMAScript modules support
- New experimental OpenTelemetry metrics output
- Blob support in experimental websockets module
- Consolidate cloud features and commands under
k6 cloud
- Breaking change: remove magic URL resolutions
Breaking changes
Require is now specification compliant and always resolves based on the file it is written in #3534
The require
function in k6 used to resolve identifiers based on the current “root of execution” (more on that later). In a lot of cases, that aligns with the file the require
is written in or a file in the same folder, which leads to the same result. In a small subset of cases, this isn’t the case.
In every other implementation, and more or less by the CommonJS specification, require
should always be relative to the file it is written in.
This also aligns with how ESM and dynamic import
also work. In order to align with them require
now uses the same underlying implementation.
There was a warning message for the last 2 releases trying to tease out cases where that would be problematic.
"root of execution" explanation
This is very much an implementation detail that has leaked and likely a not intended one.
Whenever a file is require
-ed it becomes the “root of execution”, and both require
and open
become relative to it. Once the require
finishes, the previous “root of execution” gets restored. Outside of the init
context execution, the main file is the “root of execution”.
Example:
Have 3 files: main.js
const s = require('./A/a.js');
if (s() != 5) {
throw 'Bad';
}
module.exports.default = () => {}; // just for k6 to not error
/A/a.js:
module.exports = function () {
return require('./b.js');
};
/A/b.js
module.exports = 5;
In this example when require
is called in /A/a.js
the main.js
is once again the “root of execution”. If you call the function in /A/a.js
just after defining it though, it will work as expected.
You can use the newly added import.meta.resolve()
function if you want to create a path that is relevant to the currently calling module. That will let you call it outside of a helper class and provide the path to it. Refer to docs for more details.
ECMAScript Modules (ESM) Native Support related breaking changes
As part of the ESM native support implementation, two common broken patterns in the ecosystem became apparent.
One is arguably a developer experience improvement, and the other is a consequence of the previous implementation.
Mixing CommonJS and ESM
Previously, k6 used a transpiler (Babel) internally to transpile ESM syntax to CommonJS. That led to all code always being CommonJS, and if you had CommonJS next to it, Babel would not complain.
As k6 (or the underlying JS VM implementation) did not understand ESM in itself and that CommonJS is a 100% during execution feature, this was not easy to detect or prevent.
We added a warning in v0.52.0 to give users time for migration.
To fix this - all you need is to stick to either CommonJS or ESM within each file.
Code examples and proposed changes
import { sleep } from "k6";
module.exports.default = func() { ...}
In the example above both ESM and CommonJS are used in the same file.
You can either replace:
module.exports.default = func() {}
With the ESM syntax:
export default func() {}
Or replace:
import { sleep } from 'k6';
With CommonJS:
const sleep = require('k6').sleep;
Imported identifier that can’t be resolved are now errors
Previous to this, if you were using the ESM syntax and imported the foo
identifier, but the exporting file didn’t export it, there wouldn’t be an error.
bar.js:
export const notfoo = 5;
main.js
import { foo } from './bar.js';
export default function () {
foo.bar(); // throws exception here
}
The example would not error out, but when it is accessed, there would be an exception as foo
would be undefined
.
With native ESM support, that is an error as defined by the specification and will occur sooner.
This arguably improves UX/DX, but we have reports that some users have imports like this but do not use them. So, they wouldn’t be getting exceptions, but they would now get errors.
The solution, in this case, is to stop importing the not exported identifiers.
No more “magic” URL resolution
For a long time, k6 has supported special magic URLs that aren’t really that.
Those were URLs without a scheme that:
- Started with
github.com
, and if pasted to a browser won’t open to a file. Their appeal was that you can more easily write them by hand if you know the path within a GitHub repo. - Started with
cdnjs.com
, and if pasted to a browser will open a web page with all the versions of the library. The appeal here is that you will get the latest version.
Both of them had problems though.
The GitHub ones seemed to have never been used by users, likely because you need to guess what the path should look like, and you can always just go get a real URL to the raw file.
While the cdnjs ones have some more usage, they are both a lot more complicated to support, as they require multiple requests to figure out what needs to be loaded. They also change over time. In addition the only known use at the moment is based on a very old example from an issue and it is even pointing to concrete, old version, of a library.
Given that this can be done with a normal URL, we have decided to drop support for this and have warned users for the last couple of versions.
Deprecated k6/experimental/tracing
in favor of a JavaScript implementation
k6/experimental/tracing
is arguably not very well named, and there is a good chance we would like to use the name for actual trace and span support within k6 in the future.
On top of that it can now be fully supported in js code, which is why http-instrumentation-tempo was created.
The JavaScript implementation is a drop-in replacement, so all you need to do is replace k6/experimental/tracing
with https://jslib.k6.io/http-instrumentation-tempo/1.0.0/index.js
.
The module is planned to be removed in v0.55.0, planned for November 11th, 2024.
Experimental websockets now require binaryType
to be set to receive binary messages
As part of the stabilization of the k6/experimental/websockets
we need to move the default value of binaryType
to blob
. It was previously arraybuffer
and since the last version there was a warning that it needs to be set in order for binary messages to be received.
That warning is now an error.
In the future we will move the default value to blob
and remove the error.
New features
The new features include:
- Native ESM support, which also brings some quality of life JavaScript features
- Blob support in the experimental websockets module
- Experimental OpenTelemetry metrics output
- Consolidating cloud related commands and features under
k6 cloud
Native ESM support #3456
With this feature k6 is now ES6+ compliant natively. Which means (asterisk free) support for the spread operator with object, private class fields and optional chaining
But also faster startup times, more consistent errors and easier addition of features as we now only need to add them to Sobek instead of also them being supported in the internal Babel.
History of compatibility mode and ECMAScript specification compliance
Some history: More than 6 years ago k6 started using core-js and babel to get ES6+ features. core-js is a implementation of a lot of the types and their features such as String.prototype.matchAll
among other things, and Babel gets one piece of code that uses some syntax and returns a piece of code doing the same thing (mostly) but with different syntax. Usually with the idea of supporting newer syntax but returning code that can run on runtimes which only support old syntax.
This is great, but it means that:
- For core-js each VU needs to run a bunch of JS code each initialization so it can polyfill everything that is missing
- Babel needs to be parsed and loaded and then given files to transpile on each start.
Both of those aren’t that big problems usually, but the runtime k6 uses is fairly fast, but isn’t V8. What it lacks in speed it gets back in being easy to interact with from Go, the language k6 is written in.
But it means that now on each start it needs to do a bunch of work that adds up.
So long time ago for people who would want to not have to do this we added compatibility-mode=base. This allowed you to potentially not use this features and get a big speedup. Or use them outside of k6 and likely still get significant speed up if you cut down on it.
At the same time the author and maintainer of the JS runtime we used (goja) did implement a big portion of what we were missing from core-js and also Babel. After some experiments to cut down the core-js we import we ended up contributing back the remaining parts and dropping the whole library. Which lead to 5 times reduction of memory per VU for simple scripts. And even for fairly complicated ones.
With this in mind we did try to cut down Babel as well and contribute back the simpler things it was used for. This over the years lead to small pieces of what Babel did being moved to goja and then disabled in Babel. Some of those were just easy wins, some of those were things that had very bad pathological cases where using a particular syntax made transpilation times explode.
In all of that work there always were small (or not so small) breaking changes due to many factors - sometimes our new implementation was slightly wrong and we needed to fix, sometimes more than what was in the standard was enabled in core-js or Babel, sometimes the standard changed on those. And sometimes the implementation in Babel or core-js wasn’t as full and didn’t account for all corner cases.
ECMAScript Modules(ESM) is the last such feature that Babel was used for. It also happens to be likely the one most people used, due to the fact that it is the standard way to reuse code and import libraries.
While the work on this feature started over 2 years ago, it both depended on other features that weren’t there yet, but also interacts with more or less every other feature that is part of the ECMAScript standard.
Along the way there were many internal refactors as well as additional tests to make certain we can be as backwards compatible as possible. But there also ended up being things that just weren’t going to be compatible, like the listed breaking changes.
After ESM now being natively supported, compatibility-mode base
vs extended
has only 1 feature difference - aliasing global
to globalThis
to make it a bit more compatible with (old) Node.js. There is ongoing discussion if that as well should be removed.
For the purposes of having less intrusive changes and shipping this earlier a few things have not been implemented in k6. That includes top-level-await and dynamic import support. Both of them are likely to land in the next version.
import.meta.resolve()
gets an URL from a relative path the same way import
or require
does #3873
As part of the move to ESM a lot of cases where k6 currently do not resolve the same relative path to the same file were found. Some of those were fixed - as those in require
, but others haven’t.
It also became apparent some users do use the relativity of require
, but also open
. As we move to make this consistent among uses, we decided to let users have a better transition path forward.
Using import.meta.resolve
will give you just a new URL that can be used in all functions and it will give you the same result.
import.meta.resolve
uses the same algorithm and relativity as ESM import syntax. Refer to docs for more details.
Blob support in the experimental websockets module grafana/xk6-websockets#74
In order to support the default WebSocket.binaryType
type as per spec ("blob"
), we have added support for the Blob
interface as part of the features included in the xk6-websockets
module.
So, from now on it can be used with import { Blob } from "k6/experimental/websockets";
. In the future, apart from graduating this module to stable, we might also want to expose the Blob
interface globally (no imports will be required). But for now, please remind that its support is still experimental, as the entire module is. Refer to the docs for more details.
Experimental OpenTelemetry Output #3834
This release introduces a new experimental output for OpenTelemetry. This allows users to send k6 metrics to any OpenTelemetry-compatible backends. More details and usage examples can be found in the documentation.
To output metrics to OpenTelemetry, use the experimental-opentelemetry
output option:
k6 run -o experimental-opentelemetry examples/script.js
If you have any feedback or issues, please let us know directly in the extension repository.
Consolidating cloud features under k6 cloud
#3813
This release introduces the first iteration of the revamped cloud-related commands under the k6 cloud
command, featuring two new subcommands:
k6 cloud login
: replacesk6 login cloud
for authenticating with the cloud service. It supports token-based authentication only. The previous authentication method using email and password will still be available through the legacyk6 login cloud
command, which is now deprecated and will be removed in a future release (no removal date set yet).k6 cloud run
: is the new official way to run k6 on the cloud service, serving as an alternative to the existingk6 cloud
command. Thek6 cloud
command will remain available for a few more versions but will eventually function only as a wrapper for all cloud-related commands, without any direct functionality.
UX improvements and enhancements
- #3783 Set correct exit code on invalid configurations. Thank you @ariasmn!
- #3686 Adjust logging of the executor lack of work. Thank you @athishaves!
Bug fixes
- #3746 Fix tags for metrics from gRPC streams. Thank you @cchamplin!
- #3845 Fix logging to file sometimes missing lines. Thank you @roobre!
- browser#1391 Fix race conditions in internal event handling.
Maintenance and internal improvements
- #3792, #3817, #3863 Finalize moving to Sobek, our fork of goja.
- #3815, #3840, #3821, #3836, #3840, #3844, #3821 Update dependencies.
- #3830, #3831, #3862 Refactoring and cleanup around ESM PR.
- browser#1389 Move deserialization of
BrowserContextOptions
into mapping layer. - #3841 Add browser and cloud support information to the README. Thank you @sniku!
- #3843, #3847
k6/experimental/timers
deprecation warning updates. - #3851 Simplify gRPC streams metrics tags tests.
- #3870 Update tests to work with go1.22.
Roadmap
Future breaking changes
Experimental browser module removal
In the previous release, the browser module graduated from experimental to stable. The k6/experimental/browser
module will be removed in v0.54.0
. To keep your scripts working you need to migrate to the k6/browser
module.
Experimental timers module removal
The experimental timers module has been deprecated for a few versions. It both has a stable import path k6/timers
, but also all of it’s current exports are available globally.
In the next version v0.54.0
the experimental timers module will be removed.
Experimental tracing module removal
The experimental tracing module is deprecated in this version. In two versions(v0.55.0
) the experimental module will be removed.
To keep your scripts working you need to migrate to http-instrumentation-tempo jslib.
StatsD removal
In this release, we also fixed the version where we will remove the StatsD output. The StatsD output is going to be removed in the v0.55.0
release. If you are using the StatsD output, please consider migrating to the extension LeonAdato/xk6-output-statsd.
Potentially dropping global
from extended
compatibility-mode
Currently global
is aliased to globalThis
when extended
compatibility-mode is used. This is currently the only difference with the base
compatibility-mode.
Given that this seems to have very low usage it might be dropped in the future. See the issue for more info or if you want to comment on this.