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

Reworks "life cycle" explanation page #624

Closed
wants to merge 15 commits into from
Closed

Conversation

MattDodsonEnglish
Copy link
Contributor

@MattDodsonEnglish MattDodsonEnglish commented Apr 18, 2022

This is a first attempt to clarify the very important explanation page "Stages of a test life cycle."

There is still much room to improve and expand, but I hope this current edit makes it a little easier for first-time users to conceptualize.

Since I added some content, I'd appreciate a second pair of eyes.


Cleaned up and merged in
#638

@github-actions
Copy link
Contributor

There's a version of the docs published here:

https://mdr-ci.staging.k6.io/docs/refs/pull/624/merge

It will be deleted automatically in 30 days.


| Test stage | Used to | Example | Called | Required? |
|-----------------|------------------------------------------------------------|-----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|-----------|
| **1. init** | Load local files, import modules, declare global variables | Open JSON file, Import module | Once per VU\* | Optional |
Copy link
Collaborator

Choose a reason for hiding this comment

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

Init is optional, but a test without importing any k6 API is useless. Should Init be required ??

Copy link
Contributor

Choose a reason for hiding this comment

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

Init isn't optional. You always need init code as this is the place that defines which function is the default ;).
All the code that is outside of a function will be executed always at the begining, including if a function is called

function one() {
  // this code is not init context and isn't called so it won't be executed 
}
function two() {
  // this code is not the init context but is called
}

two() // this is in the init context and calles the function `two` so it's code will also be executed

// export default function() {} // if this isn't uncommented k6 will give you error because nothing was defined as the function to be executed 

export function setup() { // this line that is in the "init code" is what defines that there is `setup` to be called
  // some setup code
} 

I hope this makes it clear that "init code" is totally not optional ;).

This actually is one of the reason (IMO) that we don't call it init code, but init context. As that moves the thinking away from code and to where the code is being executed (in the init context).

I would expect there is probably even a better way to explain this using words that are more specific and don't let so much for interpretation.

WDYT @na--

Copy link
Member

@na-- na-- Apr 19, 2022

Choose a reason for hiding this comment

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

I agree with you completely @mstoykov, though I don't have any better suggestions than "init context" 😞

Not sure if we should (or even can) coin some better terms here, but an explanation of how k6 executes scripts and why init code is executed by every VU is probably in order here, not just a slight re-wording of the existing docs 🤔

Copy link
Collaborator

@ppcano ppcano left a comment

Choose a reason for hiding this comment

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

more comments

Comment on lines 67 to 78
This is a good question, and the answer is: we do.
But code *inside* and *outside* your `default` function do different things.

Code inside `default` is called *VU code*.
VU code runs over and over through the test duration.
Besides setup and teardown, code outside of `default` is *init code*.
It runs only once per VU.

VU code can make HTTP requests, emit metrics, and generally do everything you'd expect a load test to do.
There are a few important exceptions. VU code:
* *Does not* load files from your local filesystem,
* *Does not* import any other modules.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that this explains what you can do and where, but not why which arguably is what the question above asks.

The short answer is that "we need the script to be executed in order to know what k6 should do". That includes:

  • getting hte configuration
  • knowing which functions to be called on setup/teardown and iterations
  • all the things above that you have mentioned above are also separated in order to
    • on one side not generate metrics whenever we have to run the "init code" and to be able to do it fairly repeatable
    • getting an archive with everything needed to run the code somewhere else (like in the cloud)

And that last two points is where a lot of the restrictions come from. But ultimately we just use the fact that we need to be able to run "init code" to get the options and the definitions to do that.

Also, ultimately running

let b = 5;

twice in the same javascript context will give you an error as b has already been defined. Putting it in the function and calling the function is what makes a new context and let you do

function f () {
  let res = http.request()
  // something with res
}
f();
f();

So there is also kind of another technical reason for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for these explanations! I think you are getting at something very essential to keep in mind when explaining these internals.

We always must ask: what does this information mean to the user? What bad thing will happen if they don't know this information?

It's tricky sometimes to separate what information matters for implementers from what matters for end users.

Co-authored-by: Mihail Stoykov <[email protected]>
@MattDodsonEnglish
Copy link
Contributor Author

#624 (comment)
#624 (comment)

I'm unthreading the discussions for these , because I think the conversation is converging on the same essential point.

As @na-- writes

, but an explanation of how k6 executes scripts and why init code is executed by every VU is probably in order

I think separating "How" and "Why" makes a lot of sense. I will try:


How does k6 execute scripts?

  1. Code in the init context prepares the script. During this preparation, the init context
    • Imports modules
    • Loads files from the local filesystem
    • initializes all VUs, configuring their options
    • defines the functions for default (VU), setup, and teardown stages.
  2. (Optional) Setup code runs, preprocessing data and setting up the test environment.
  3. VU code runs, as defined in the default functions. This code can run multiple times, for however much or as long as the options define.
  4. (Optional) Teardown code runs, postprocessing data and closing the test environment.

Why does k6 work this way?

There are multiple reasons to separate the code into these stages.

  • Stabler, more meaningful test results.

    As init code is separated, it doesn't generate metrics.
    This isolation removes irrelevant computation from the actual VU requests, making test results more accurate.

  • More portable design

    The separation lets k6 create an archive and run the script in a different execution mode, e.g. in the cloud.
    The init context already defines which files to load and which modules to import.

    In more sophisticated execution, like clustered systems, this separation brings further performance benefits―certain nodes don't even need writable filesystems.
    Everything can be kept in memory.

  • Functions can be reused

    The init context defines functions for the test script.
    These functions can be called multiple times during the test execution, with each call in a new context.


@na-- @ppcano @mstoykov

What do you think about this rewrite? It's simpler, and I hope more accurate. Perhaps this could replace the current intro and the sections called "Init and VU context" and "Benefits of Separation".

@ppcano
Copy link
Collaborator

ppcano commented Apr 19, 2022

How does k6 execute scripts? great.

Why k6 works this way is very advanced and too detailed. I think it should not be shown upfront. I suggest using either an internal page or the "collapsible component".

  • Functions can be reused: unsure about this one.

@MattDodsonEnglish
Copy link
Contributor Author

Why k6 works this way is very advanced and too detailed. I think it should not be shown upfront. I suggest using either an internal page or the "collapsible component".

This comment made me think that the whole article could be restructured and simplified. Basically, give the very TL,DR info in the intro. Then, use the table to go into a bit more detail. After that, explain each stage. Finally, provide the reasons why this works this way at the end.

This would give the whole document a structure of progressive disclosure. Generally it's good to leave the most technical details at the end.

# The Test lifecycle
<Intro: same content as "How does k6 execute scripts>
<code stub>
## Essential information about each test stage
<Table> 
## The init context
<Information about what init does. I think this is the only content that's missing>
## VU code runs in the default function
### The default function lifecycle
## Setup and teardown code
## Why k6 works this way


To support these modes, you can pass only data (i.e. JSON) between `setup()` and the
other stages.
You can pass only data (i.e. JSON) between `setup()` and the other stages.
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a note that it's a copy of what setup returns. So that it:

  1. uses more memory if it's big
  2. you can't actually change it in the VU code and see the changes in the teardown. Although any VU will see it's changes to the data. This though is likely to change as we might freeze the whole object just to make it less obvious that it won't work

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Although any VU will see it's changes to the data. This though is likely to change as we might freeze the whole object just to make it less obvious that it won't work

@mstoykov

Sorry, I'm a bit confused here. So, VU code can manipulate data from setup, but it can't pass that data to teardown. Is that correct?

And, in the table, I note that setup code can be used to " share data among VUs". How is this data shared. Is it from one VU to the next VU? Or does each VU grab "clean" data from the setup code?

However it works, it seems like, if VU code can manipulate data that doesn't end up in the teardown stage, then the data is not really "passed" from Setup to VU to Default.

Rather, setup passes data in two places. Once, directly to teardown. And once (or many times??) directly to default.

I've made a small diagram to illustrate how I conceive this. Excuse me, I'm sure said some inaccurate things here, but I thought it may be easier to correct than to explain :-) .

image

Copy link
Contributor

@mstoykov mstoykov Apr 20, 2022

Choose a reason for hiding this comment

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

more or less -yes to all of those :).

In practice implementing data being modified and all that modifications being visible by :

  1. all VUs
  2. teardown

Will be ... extremely complicated and (computationally) expensive especially in distributed execution. Which is why we haven't even discussed it ever. But long long time ago I made fixes so that they have separate copies - previous to that this was sometimes panicking as multiple VUs were writing to the same not thread safe structure.

edit: the data is passed to each VU separately - but that shouldn't really be depended upon or documented. We do in general really want if it's a singel copy that is just unmodifiable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Haha, this sounds pretty complicated. I think I'm just going to just suggest that it's a "copy," and leave it at that. Especially since it sounds like this data may get "frozen" in the VU stage in some later k6 version anyway.

For the people who are motivated enough to actually use this implementation in some advanced way, well they can look at the source code (and find this discussion). 🙂

@MattDodsonEnglish
Copy link
Contributor Author

@ppcano @mstoykov

With 534db8c , this page is almost at a complete rewrite now.

I tried to add progressively more detail. Hopefully, the page gives readers a solid mental model of how a k6 script runs, without adding too much technical details or suggesting complex/impossible operations (e.g passing and manipulating data from setup to VU to teardown).

I'd like the diagram to be more beautiful, and I'd prefer not having the "description list" in pure HTML, but fixing those two issues would require some time investment.

If you all think it looks alright, I'd like to merge this.

@ppcano ppcano mentioned this pull request Apr 25, 2022
@MattDodsonEnglish
Copy link
Contributor Author

See:

#638

@MattDodsonEnglish MattDodsonEnglish deleted the lifecycle-edits branch May 10, 2022 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants