diff --git a/core/local/eventloop_test.go b/cmd/integration_tests/eventloop/eventloop_test.go similarity index 73% rename from core/local/eventloop_test.go rename to cmd/integration_tests/eventloop/eventloop_test.go index 887ab22b525..febdb751d45 100644 --- a/core/local/eventloop_test.go +++ b/cmd/integration_tests/eventloop/eventloop_test.go @@ -1,4 +1,4 @@ -package local +package tests import ( "context" @@ -9,12 +9,19 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" + "go.k6.io/k6/cmd/integration_tests/testmodules/events" + "go.k6.io/k6/core/local" "go.k6.io/k6/js" + "go.k6.io/k6/js/modules" "go.k6.io/k6/lib" + "go.k6.io/k6/lib/executor" "go.k6.io/k6/lib/metrics" "go.k6.io/k6/lib/testutils" + "go.k6.io/k6/lib/testutils/minirunner" "go.k6.io/k6/lib/types" "go.k6.io/k6/loader" + "go.k6.io/k6/stats" + "gopkg.in/guregu/null.v3" ) func eventLoopTest(t *testing.T, script []byte, testHandle func(context.Context, lib.Runner, error, *testutils.SimpleLogrusHook)) { @@ -23,7 +30,7 @@ func eventLoopTest(t *testing.T, script []byte, testHandle func(context.Context, logHook := &testutils.SimpleLogrusHook{HookedLevels: []logrus.Level{logrus.InfoLevel, logrus.WarnLevel, logrus.ErrorLevel}} logger.AddHook(logHook) - script = []byte(`import {setTimeout} from "k6/experimental"; + script = []byte(`import {setTimeout} from "k6/x/events"; ` + string(script)) registry := metrics.NewRegistry() builtinMetrics := metrics.RegisterBuiltinMetrics(registry) @@ -58,6 +65,10 @@ func eventLoopTest(t *testing.T, script []byte, testHandle func(context.Context, } } +func init() { + modules.Register("k6/x/events", events.New()) +} + func TestEventLoop(t *testing.T) { t.Parallel() script := []byte(` @@ -147,7 +158,10 @@ export default function() { for i, entry := range entries { msgs[i] = entry.Message } - require.Equal(t, []string{"second"}, msgs) + require.Equal(t, []string{ + "setTimeout 1 was stopped because the VU iteration was interrupted", + "second", + }, msgs) }) } @@ -178,6 +192,48 @@ export default function() { for i, entry := range entries { msgs[i] = entry.Message } - require.Equal(t, []string{"just error\n\tat /script.js:13:4(15)\n\tat native\n", "1"}, msgs) + require.Equal(t, []string{ + "setTimeout 1 was stopped because the VU iteration was interrupted", + "just error\n\tat /script.js:13:4(15)\n\tat native\n", "1", + }, msgs) }) } + +func newTestExecutionScheduler( + t *testing.T, runner lib.Runner, logger *logrus.Logger, opts lib.Options, +) (ctx context.Context, cancel func(), execScheduler *local.ExecutionScheduler, samples chan stats.SampleContainer) { + if runner == nil { + runner = &minirunner.MiniRunner{} + } + ctx, cancel = context.WithCancel(context.Background()) + newOpts, err := executor.DeriveScenariosFromShortcuts(lib.Options{ + MetricSamplesBufferSize: null.NewInt(200, false), + }.Apply(runner.GetOptions()).Apply(opts), nil) + require.NoError(t, err) + require.Empty(t, newOpts.Validate()) + + require.NoError(t, runner.SetOptions(newOpts)) + + if logger == nil { + logger = logrus.New() + logger.SetOutput(testutils.NewTestOutput(t)) + } + + execScheduler, err = local.NewExecutionScheduler(runner, logger) + require.NoError(t, err) + + samples = make(chan stats.SampleContainer, newOpts.MetricSamplesBufferSize.Int64) + go func() { + for { + select { + case <-samples: + case <-ctx.Done(): + return + } + } + }() + + require.NoError(t, execScheduler.Init(ctx, samples)) + + return ctx, cancel, execScheduler, samples +} diff --git a/cmd/integration_tests/testmodules/events/events.go b/cmd/integration_tests/testmodules/events/events.go new file mode 100644 index 00000000000..e34c087c083 --- /dev/null +++ b/cmd/integration_tests/testmodules/events/events.go @@ -0,0 +1,157 @@ +// Package events implements setInterval, setTimeout and co. Not to be used, mostly for testing purposes +package events + +import ( + "sync" + "sync/atomic" + "time" + + "github.com/dop251/goja" + "go.k6.io/k6/js/modules" +) + +// RootModule is the global module instance that will create module +// instances for each VU. +type RootModule struct{} + +// Events represents an instance of the events module. +type Events struct { + vu modules.VU + + timerStopCounter uint32 + timerStopsLock sync.Mutex + timerStops map[uint32]chan struct{} +} + +var ( + _ modules.Module = &RootModule{} + _ modules.Instance = &Events{} +) + +// New returns a pointer to a new RootModule instance. +func New() *RootModule { + return &RootModule{} +} + +// NewModuleInstance implements the modules.Module interface to return +// a new instance for each VU. +func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance { + return &Events{ + vu: vu, + timerStops: make(map[uint32]chan struct{}), + } +} + +// Exports returns the exports of the k6 module. +func (e *Events) Exports() modules.Exports { + return modules.Exports{ + Named: map[string]interface{}{ + "setTimeout": e.setTimeout, + "clearTimeout": e.clearTimeout, + "setInterval": e.setInterval, + "clearInterval": e.clearInterval, + }, + } +} + +func noop() error { return nil } + +func (e *Events) getTimerStopCh() (uint32, chan struct{}) { + id := atomic.AddUint32(&e.timerStopCounter, 1) + ch := make(chan struct{}) + e.timerStopsLock.Lock() + e.timerStops[id] = ch + e.timerStopsLock.Unlock() + return id, ch +} + +func (e *Events) stopTimerCh(id uint32) bool { //nolint:unparam + e.timerStopsLock.Lock() + defer e.timerStopsLock.Unlock() + ch, ok := e.timerStops[id] + if !ok { + return false + } + delete(e.timerStops, id) + close(ch) + return true +} + +func (e *Events) call(callback goja.Callable, args []goja.Value) error { + // TODO: investigate, not sure GlobalObject() is always the correct value for `this`? + _, err := callback(e.vu.Runtime().GlobalObject(), args...) + return err +} + +func (e *Events) setTimeout(callback goja.Callable, delay float64, args ...goja.Value) uint32 { + runOnLoop := e.vu.RegisterCallback() + id, stopCh := e.getTimerStopCh() + + if delay < 0 { + delay = 0 + } + + go func() { + timer := time.NewTimer(time.Duration(delay * float64(time.Millisecond))) + defer func() { + e.stopTimerCh(id) + if !timer.Stop() { + <-timer.C + } + }() + + select { + case <-timer.C: + runOnLoop(func() error { + return e.call(callback, args) + }) + case <-stopCh: + runOnLoop(noop) + case <-e.vu.Context().Done(): + e.vu.State().Logger.Warnf("setTimeout %d was stopped because the VU iteration was interrupted", id) + runOnLoop(noop) + } + }() + + return id +} + +func (e *Events) clearTimeout(id uint32) { + e.stopTimerCh(id) +} + +func (e *Events) setInterval(callback goja.Callable, delay float64, args ...goja.Value) uint32 { + runOnLoop := e.vu.RegisterCallback() + id, stopCh := e.getTimerStopCh() + + go func() { + ticker := time.NewTicker(time.Duration(delay * float64(time.Millisecond))) + defer func() { + e.stopTimerCh(id) + ticker.Stop() + }() + + for { + select { + case <-ticker.C: + runOnLoop(func() error { + runOnLoop = e.vu.RegisterCallback() + return e.call(callback, args) + }) + case <-stopCh: + runOnLoop(noop) + return + case <-e.vu.Context().Done(): + e.vu.State().Logger.Warnf("setInterval %d was stopped because the VU iteration was interrupted", id) + runOnLoop(noop) + return + } + } + }() + + return id +} + +func (e *Events) clearInterval(id uint32) { + e.stopTimerCh(id) +} diff --git a/cmd/integration_tests/testmodules/events/events.js b/cmd/integration_tests/testmodules/events/events.js new file mode 100644 index 00000000000..7e5c98fd6e6 --- /dev/null +++ b/cmd/integration_tests/testmodules/events/events.js @@ -0,0 +1,49 @@ +import exec from 'k6/execution'; +import { setTimeout, clearTimeout, setInterval, clearInterval } from 'k6/events' + + +export let options = { + scenarios: { + 'foo': { + executor: 'constant-vus', + vus: 1, + duration: '3.8s', + gracefulStop: '0s', + } + } +}; + +function debug(arg) { + let t = String((new Date()) - exec.scenario.startTime).padStart(6, ' ') + console.log(`[${t}ms, iter=${exec.scenario.iterationInTest}] ${arg}`); +} + +export default function () { + debug('default start'); + + let tickCount = 1; + let f0 = (arg) => { + debug(`${arg} ${tickCount++}`); + } + let t0 = setInterval(f0, 500, 'tick') + + let f1 = (arg) => { + debug(arg); + clearInterval(t0); + } + let t1 = setTimeout(f1, 2000, 'third'); + + let t2 = setTimeout(debug, 1500, 'never happening'); + + let f3 = (arg) => { + debug(arg); + clearTimeout(t2); + setTimeout(debug, 600, 'second'); + } + let t3 = setTimeout(f3, 1000, 'first'); + + debug('default end'); + if (exec.scenario.iterationInTest == 1) { + debug(`expected last iter, the interval ID is ${t0}, we also expect timer ${t1} to be interrupted`) + } +} diff --git a/cmd/integration_tests/testmodules/events/events_test.go b/cmd/integration_tests/testmodules/events/events_test.go new file mode 100644 index 00000000000..bdcb57cbf3d --- /dev/null +++ b/cmd/integration_tests/testmodules/events/events_test.go @@ -0,0 +1,43 @@ +package events + +import ( + "context" + "testing" + + "github.com/dop251/goja" + "github.com/stretchr/testify/require" + "go.k6.io/k6/js/common" + "go.k6.io/k6/js/eventloop" + "go.k6.io/k6/js/modulestest" +) + +func TestSetTimeout(t *testing.T) { + t.Parallel() + rt := goja.New() + vu := &modulestest.VU{ + RuntimeField: rt, + InitEnvField: &common.InitEnvironment{}, + CtxField: context.Background(), + StateField: nil, + } + + m, ok := New().NewModuleInstance(vu).(*Events) + require.True(t, ok) + var log []string + require.NoError(t, rt.Set("events", m.Exports().Named)) + require.NoError(t, rt.Set("print", func(s string) { log = append(log, s) })) + loop := eventloop.New(vu) + vu.RegisterCallbackField = loop.RegisterCallback + + err := loop.Start(func() error { + _, err := vu.Runtime().RunString(` + events.setTimeout(()=> { + print("in setTimeout") + }) + print("outside setTimeout") + `) + return err + }) + require.NoError(t, err) + require.Equal(t, []string{"outside setTimeout", "in setTimeout"}, log) +} diff --git a/js/bundle.go b/js/bundle.go index 65dcc919f1f..dba20a7d8e7 100644 --- a/js/bundle.go +++ b/js/bundle.go @@ -36,6 +36,7 @@ import ( "go.k6.io/k6/js/common" "go.k6.io/k6/js/compiler" + "go.k6.io/k6/js/eventloop" "go.k6.io/k6/lib" "go.k6.io/k6/lib/consts" "go.k6.io/k6/lib/metrics" @@ -331,8 +332,8 @@ func (b *Bundle) instantiate(logger logrus.FieldLogger, rt *goja.Runtime, init * init.moduleVUImpl.initEnv = initenv init.moduleVUImpl.ctx = context.Background() unbindInit := b.setInitGlobals(rt, init) - init.moduleVUImpl.eventLoop = newEventLoop(init.moduleVUImpl) - err := init.moduleVUImpl.eventLoop.start(func() error { + init.moduleVUImpl.eventLoop = eventloop.New(init.moduleVUImpl) + err := init.moduleVUImpl.eventLoop.Start(func() error { _, err := rt.RunProgram(b.Program) return err }) diff --git a/js/eventloop.go b/js/eventloop/eventloop.go similarity index 80% rename from js/eventloop.go rename to js/eventloop/eventloop.go index 77bb712fbca..230cda93dfa 100644 --- a/js/eventloop.go +++ b/js/eventloop/eventloop.go @@ -1,4 +1,5 @@ -package js +// Package eventloop implements an event loop to be used thought js and it's subpackages +package eventloop import ( "fmt" @@ -8,7 +9,7 @@ import ( "go.k6.io/k6/js/modules" ) -// eventLoop implements an event with +// EventLoop implements an event with // handling of unhandled rejected promises. // // A specific thing about this event loop is that it will wait to return @@ -20,7 +21,7 @@ import ( // but also they need to end when all the instructions are done. // Additionally because of this on any error while the event loop will exit it's // required to wait on the event loop to be empty before the execution can continue. -type eventLoop struct { +type EventLoop struct { lock sync.Mutex queue []func() error wakeupCh chan struct{} // TODO: maybe use sync.Cond ? @@ -33,10 +34,11 @@ type eventLoop struct { pendingPromiseRejections map[*goja.Promise]struct{} } -// newEventLoop returns a new event loop with a few helpers attached to it: +// New returns a new event loop with a few helpers attached to it: +// - adding setTimeout javascript implementation // - reporting (and aborting on) unhandled promise rejections -func newEventLoop(vu modules.VU) *eventLoop { - e := &eventLoop{ +func New(vu modules.VU) *EventLoop { + e := &EventLoop{ wakeupCh: make(chan struct{}, 1), pendingPromiseRejections: make(map[*goja.Promise]struct{}), vu: vu, @@ -46,18 +48,18 @@ func newEventLoop(vu modules.VU) *eventLoop { return e } -func (e *eventLoop) wakeup() { +func (e *EventLoop) wakeup() { select { case e.wakeupCh <- struct{}{}: default: } } -// registerCallback register that a callback will be invoked on the loop, preventing it from returning/finishing. +// RegisterCallback register that a callback will be invoked on the loop, preventing it from returning/finishing. // The returned function, upon invocation, will queue its argument and wakeup the loop if needed. // If the eventLoop has since stopped, it will not be executed. // This function *must* be called from within running on the event loop, but its result can be called from anywhere. -func (e *eventLoop) registerCallback() func(func() error) { +func (e *EventLoop) RegisterCallback() func(func() error) { e.lock.Lock() e.registeredCallbacks++ e.lock.Unlock() @@ -71,7 +73,7 @@ func (e *eventLoop) registerCallback() func(func() error) { } } -func (e *eventLoop) promiseRejectionTracker(p *goja.Promise, op goja.PromiseRejectionOperation) { +func (e *EventLoop) promiseRejectionTracker(p *goja.Promise, op goja.PromiseRejectionOperation) { // No locking necessary here as the goja runtime will call this synchronously // Read Notes on https://tc39.es/ecma262/#sec-host-promise-rejection-tracker if op == goja.PromiseRejectionReject { @@ -81,7 +83,7 @@ func (e *eventLoop) promiseRejectionTracker(p *goja.Promise, op goja.PromiseReje } } -func (e *eventLoop) popAll() (queue []func() error, awaiting bool) { +func (e *EventLoop) popAll() (queue []func() error, awaiting bool) { e.lock.Lock() queue = e.queue e.queue = make([]func() error, 0, len(queue)) @@ -90,10 +92,10 @@ func (e *eventLoop) popAll() (queue []func() error, awaiting bool) { return } -// start will run the event loop until it's empty and there are no uninvoked registered callbacks +// Start will run the event loop until it's empty and there are no uninvoked registered callbacks // or a queued function returns an error. The provided firstCallback will be the first thing executed. -// After start returns the event loop can be reused as long as waitOnRegistered is called. -func (e *eventLoop) start(firstCallback func() error) error { +// After Start returns the event loop can be reused as long as waitOnRegistered is called. +func (e *EventLoop) Start(firstCallback func() error) error { e.queue = []func() error{firstCallback} for { queue, awaiting := e.popAll() @@ -129,8 +131,8 @@ func (e *eventLoop) start(firstCallback func() error) error { } } -// Wait on all registered callbacks so we know nothing is still doing work. -func (e *eventLoop) waitOnRegistered() { +// WaitOnRegistered waits on all registered callbacks so we know nothing is still doing work. +func (e *EventLoop) WaitOnRegistered() { for { _, awaiting := e.popAll() if !awaiting { diff --git a/js/eventloop_test.go b/js/eventloop/eventloop_test.go similarity index 74% rename from js/eventloop_test.go rename to js/eventloop/eventloop_test.go index 7fed52b58e5..089657e51ac 100644 --- a/js/eventloop_test.go +++ b/js/eventloop/eventloop_test.go @@ -1,4 +1,4 @@ -package js +package eventloop import ( "errors" @@ -13,19 +13,19 @@ import ( func TestBasicEventLoop(t *testing.T) { t.Parallel() - loop := newEventLoop(&modulestest.VU{RuntimeField: goja.New()}) + loop := New(&modulestest.VU{RuntimeField: goja.New()}) var ran int f := func() error { //nolint:unparam ran++ return nil } - require.NoError(t, loop.start(f)) + require.NoError(t, loop.Start(f)) require.Equal(t, 1, ran) - require.NoError(t, loop.start(f)) + require.NoError(t, loop.Start(f)) require.Equal(t, 2, ran) - require.Error(t, loop.start(func() error { + require.Error(t, loop.Start(func() error { _ = f() - loop.registerCallback()(f) + loop.RegisterCallback()(f) return errors.New("something") })) require.Equal(t, 3, ran) @@ -33,11 +33,11 @@ func TestBasicEventLoop(t *testing.T) { func TestEventLoopRegistered(t *testing.T) { t.Parallel() - loop := newEventLoop(&modulestest.VU{RuntimeField: goja.New()}) + loop := New(&modulestest.VU{RuntimeField: goja.New()}) var ran int f := func() error { ran++ - r := loop.registerCallback() + r := loop.RegisterCallback() go func() { time.Sleep(time.Second) r(func() error { @@ -48,7 +48,7 @@ func TestEventLoopRegistered(t *testing.T) { return nil } start := time.Now() - require.NoError(t, loop.start(f)) + require.NoError(t, loop.Start(f)) took := time.Since(start) require.Equal(t, 2, ran) require.Less(t, time.Second, took) @@ -58,10 +58,10 @@ func TestEventLoopRegistered(t *testing.T) { func TestEventLoopWaitOnRegistered(t *testing.T) { t.Parallel() var ran int - loop := newEventLoop(&modulestest.VU{RuntimeField: goja.New()}) + loop := New(&modulestest.VU{RuntimeField: goja.New()}) f := func() error { ran++ - r := loop.registerCallback() + r := loop.RegisterCallback() go func() { time.Sleep(time.Second) r(func() error { @@ -72,9 +72,9 @@ func TestEventLoopWaitOnRegistered(t *testing.T) { return fmt.Errorf("expected") } start := time.Now() - require.Error(t, loop.start(f)) + require.Error(t, loop.Start(f)) took := time.Since(start) - loop.waitOnRegistered() + loop.WaitOnRegistered() took2 := time.Since(start) require.Equal(t, 1, ran) require.Greater(t, time.Millisecond*50, took) @@ -85,11 +85,11 @@ func TestEventLoopWaitOnRegistered(t *testing.T) { func TestEventLoopReuse(t *testing.T) { t.Parallel() sleepTime := time.Millisecond * 500 - loop := newEventLoop(&modulestest.VU{RuntimeField: goja.New()}) + loop := New(&modulestest.VU{RuntimeField: goja.New()}) f := func() error { for i := 0; i < 100; i++ { bad := i == 17 - r := loop.registerCallback() + r := loop.RegisterCallback() go func() { if !bad { @@ -107,9 +107,9 @@ func TestEventLoopReuse(t *testing.T) { } for i := 0; i < 3; i++ { start := time.Now() - require.Error(t, loop.start(f)) + require.Error(t, loop.Start(f)) took := time.Since(start) - loop.waitOnRegistered() + loop.WaitOnRegistered() took2 := time.Since(start) require.Greater(t, time.Millisecond*50, took) require.Less(t, sleepTime, took2) diff --git a/js/initcontext.go b/js/initcontext.go index b56224e3085..29878995c43 100644 --- a/js/initcontext.go +++ b/js/initcontext.go @@ -35,6 +35,7 @@ import ( "go.k6.io/k6/js/common" "go.k6.io/k6/js/compiler" + "go.k6.io/k6/js/eventloop" "go.k6.io/k6/js/modules" "go.k6.io/k6/js/modules/k6" "go.k6.io/k6/js/modules/k6/crypto" @@ -42,7 +43,6 @@ import ( "go.k6.io/k6/js/modules/k6/data" "go.k6.io/k6/js/modules/k6/encoding" "go.k6.io/k6/js/modules/k6/execution" - "go.k6.io/k6/js/modules/k6/experimental" "go.k6.io/k6/js/modules/k6/grpc" "go.k6.io/k6/js/modules/k6/html" "go.k6.io/k6/js/modules/k6/http" @@ -156,7 +156,7 @@ type moduleVUImpl struct { initEnv *common.InitEnvironment state *lib.State runtime *goja.Runtime - eventLoop *eventLoop + eventLoop *eventloop.EventLoop } func newModuleVUImpl() *moduleVUImpl { @@ -182,7 +182,7 @@ func (m *moduleVUImpl) Runtime() *goja.Runtime { } func (m *moduleVUImpl) RegisterCallback() func(func() error) { - return m.eventLoop.registerCallback() + return m.eventLoop.RegisterCallback() } /* This is here to illustrate how to use RegisterCallback to get a promise to work with the event loop @@ -375,18 +375,17 @@ func (i *InitContext) allowOnlyOpenedFiles() { func getInternalJSModules() map[string]interface{} { return map[string]interface{}{ - "k6": k6.New(), - "k6/crypto": crypto.New(), - "k6/crypto/x509": x509.New(), - "k6/data": data.New(), - "k6/encoding": encoding.New(), - "k6/execution": execution.New(), - "k6/net/grpc": grpc.New(), - "k6/html": html.New(), - "k6/http": http.New(), - "k6/metrics": metrics.New(), - "k6/ws": ws.New(), - "k6/experimental": experimental.New(), + "k6": k6.New(), + "k6/crypto": crypto.New(), + "k6/crypto/x509": x509.New(), + "k6/data": data.New(), + "k6/encoding": encoding.New(), + "k6/execution": execution.New(), + "k6/net/grpc": grpc.New(), + "k6/html": html.New(), + "k6/http": http.New(), + "k6/metrics": metrics.New(), + "k6/ws": ws.New(), } } diff --git a/js/modulestest/modulestest.go b/js/modulestest/modulestest.go index 03519a7cb60..b7161d1a776 100644 --- a/js/modulestest/modulestest.go +++ b/js/modulestest/modulestest.go @@ -33,10 +33,11 @@ var _ modules.VU = &VU{} // VU is a modules.VU implementation meant to be used within tests type VU struct { - CtxField context.Context - InitEnvField *common.InitEnvironment - StateField *lib.State - RuntimeField *goja.Runtime + CtxField context.Context + InitEnvField *common.InitEnvironment + StateField *lib.State + RuntimeField *goja.Runtime + RegisterCallbackField func() func(f func() error) } // Context returns internally set field to conform to modules.VU interface @@ -61,6 +62,5 @@ func (m *VU) Runtime() *goja.Runtime { // RegisterCallback is not really implemented func (m *VU) RegisterCallback() func(f func() error) { - // TODO Implement - return nil + return m.RegisterCallbackField() } diff --git a/js/runner.go b/js/runner.go index f470f256129..f15d071ecc4 100644 --- a/js/runner.go +++ b/js/runner.go @@ -48,6 +48,7 @@ import ( "go.k6.io/k6/errext" "go.k6.io/k6/errext/exitcodes" "go.k6.io/k6/js/common" + "go.k6.io/k6/js/eventloop" "go.k6.io/k6/lib" "go.k6.io/k6/lib/consts" "go.k6.io/k6/lib/metrics" @@ -791,9 +792,9 @@ func (u *VU) runFn( startTime := time.Now() if u.moduleVUImpl.eventLoop == nil { - u.moduleVUImpl.eventLoop = newEventLoop(u.moduleVUImpl) + u.moduleVUImpl.eventLoop = eventloop.New(u.moduleVUImpl) } - err = u.moduleVUImpl.eventLoop.start(func() (err error) { + err = u.moduleVUImpl.eventLoop.Start(func() (err error) { // here the returned value purposefully shadows the external one as they can be different defer func() { if r := recover(); r != nil { @@ -823,7 +824,7 @@ func (u *VU) runFn( if cancel != nil { cancel() - u.moduleVUImpl.eventLoop.waitOnRegistered() + u.moduleVUImpl.eventLoop.WaitOnRegistered() } endTime := time.Now() var exception *goja.Exception