Skip to content

Commit

Permalink
common.bind replacement (#2108)
Browse files Browse the repository at this point in the history
This change adds a new interface which when implemented by registered modules let them gain access to the Context, Runtime, State and in the future others without using `common.Bind` but instead through simple methods (as it probably should've always been).

Additionally, it lets defining of both default and named exports and let users more accurately name their exports instead of depending on the magic in common.Bind and goja.

Co-authored-by: Ivan Mirić <ivan@loadimpact.com>
  • Loading branch information
mstoykov and Ivan Mirić authored Aug 25, 2021
1 parent 37681ac commit 34a7743
Show file tree
Hide file tree
Showing 5 changed files with 352 additions and 69 deletions.
88 changes: 87 additions & 1 deletion js/initcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ import (
"go.k6.io/k6/js/common"
"go.k6.io/k6/js/compiler"
"go.k6.io/k6/js/modules"
"go.k6.io/k6/js/modules/k6"
"go.k6.io/k6/js/modules/k6/crypto"
"go.k6.io/k6/js/modules/k6/crypto/x509"
"go.k6.io/k6/js/modules/k6/data"
"go.k6.io/k6/js/modules/k6/encoding"
"go.k6.io/k6/js/modules/k6/grpc"
"go.k6.io/k6/js/modules/k6/html"
"go.k6.io/k6/js/modules/k6/http"
"go.k6.io/k6/js/modules/k6/metrics"
"go.k6.io/k6/js/modules/k6/ws"
"go.k6.io/k6/lib"
"go.k6.io/k6/loader"
)
Expand Down Expand Up @@ -88,7 +98,7 @@ func NewInitContext(
programs: make(map[string]programWithSource),
compatibilityMode: compatMode,
logger: logger,
modules: modules.GetJSModules(),
modules: getJSModules(),
}
}

Expand Down Expand Up @@ -140,14 +150,63 @@ func (i *InitContext) Require(arg string) goja.Value {
}
}

type moduleInstanceCoreImpl struct {
ctxPtr *context.Context
// we can technically put lib.State here as well as anything else
}

func (m *moduleInstanceCoreImpl) GetContext() context.Context {
return *m.ctxPtr
}

func (m *moduleInstanceCoreImpl) GetInitEnv() *common.InitEnvironment {
return common.GetInitEnv(*m.ctxPtr) // TODO thread it correctly instead
}

func (m *moduleInstanceCoreImpl) GetState() *lib.State {
return lib.GetState(*m.ctxPtr) // TODO thread it correctly instead
}

func (m *moduleInstanceCoreImpl) GetRuntime() *goja.Runtime {
return common.GetRuntime(*m.ctxPtr) // TODO thread it correctly instead
}

func toESModuleExports(exp modules.Exports) interface{} {
if exp.Named == nil {
return exp.Default
}
if exp.Default == nil {
return exp.Named
}

result := make(map[string]interface{}, len(exp.Named)+2)

for k, v := range exp.Named {
result[k] = v
}
// Maybe check that those weren't set
result["default"] = exp.Default
// this so babel works with the `default` when it transpiles from ESM to commonjs.
// This should probably be removed once we have support for ESM directly. So that require doesn't get support for
// that while ESM has.
result["__esModule"] = true

return result
}

func (i *InitContext) requireModule(name string) (goja.Value, error) {
mod, ok := i.modules[name]
if !ok {
return nil, fmt.Errorf("unknown module: %s", name)
}
if modV2, ok := mod.(modules.IsModuleV2); ok {
instance := modV2.NewModuleInstance(&moduleInstanceCoreImpl{ctxPtr: i.ctxPtr})
return i.runtime.ToValue(toESModuleExports(instance.GetExports())), nil
}
if perInstance, ok := mod.(modules.HasModuleInstancePerVU); ok {
mod = perInstance.NewModuleInstancePerVU()
}

return i.runtime.ToValue(common.Bind(i.runtime, mod, i.ctxPtr)), nil
}

Expand Down Expand Up @@ -255,3 +314,30 @@ func (i *InitContext) Open(ctx context.Context, filename string, args ...string)
}
return i.runtime.ToValue(string(data)), nil
}

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/net/grpc": grpc.New(),
"k6/html": html.New(),
"k6/http": http.New(),
"k6/metrics": metrics.New(),
"k6/ws": ws.New(),
}
}

func getJSModules() map[string]interface{} {
result := getInternalJSModules()
external := modules.GetJSModules()

// external is always prefixed with `k6/x`
for k, v := range external {
result[k] = v
}

return result
}
121 changes: 86 additions & 35 deletions js/modules/k6/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
package metrics

import (
"context"
"errors"
"fmt"
"regexp"
Expand All @@ -30,7 +29,7 @@ import (
"github.com/dop251/goja"

"go.k6.io/k6/js/common"
"go.k6.io/k6/lib"
"go.k6.io/k6/js/modules"
"go.k6.io/k6/stats"
)

Expand All @@ -44,41 +43,50 @@ func checkName(name string) bool {

type Metric struct {
metric *stats.Metric
core modules.InstanceCore
}

// ErrMetricsAddInInitContext is error returned when adding to metric is done in the init context
var ErrMetricsAddInInitContext = common.NewInitContextError("Adding to metrics in the init context is not supported")

func newMetric(ctxPtr *context.Context, name string, t stats.MetricType, isTime []bool) (interface{}, error) {
if lib.GetState(*ctxPtr) != nil {
func (mi *ModuleInstance) newMetric(call goja.ConstructorCall, t stats.MetricType) (*goja.Object, error) {
if mi.GetInitEnv() == nil {
return nil, errors.New("metrics must be declared in the init context")
}
rt := mi.GetRuntime()
c, _ := goja.AssertFunction(rt.ToValue(func(name string, isTime ...bool) (*goja.Object, error) {
// TODO: move verification outside the JS
if !checkName(name) {
return nil, common.NewInitContextError(fmt.Sprintf("Invalid metric name: '%s'", name))
}

// TODO: move verification outside the JS
if !checkName(name) {
return nil, common.NewInitContextError(fmt.Sprintf("Invalid metric name: '%s'", name))
}

valueType := stats.Default
if len(isTime) > 0 && isTime[0] {
valueType = stats.Time
}
valueType := stats.Default
if len(isTime) > 0 && isTime[0] {
valueType = stats.Time
}
m := stats.New(name, t, valueType)

rt := common.GetRuntime(*ctxPtr)
bound := common.Bind(rt, Metric{stats.New(name, t, valueType)}, ctxPtr)
o := rt.NewObject()
err := o.DefineDataProperty("name", rt.ToValue(name), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE)
metric := &Metric{metric: m, core: mi.InstanceCore}
o := rt.NewObject()
err := o.DefineDataProperty("name", rt.ToValue(name), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE)
if err != nil {
return nil, err
}
if err = o.Set("add", rt.ToValue(metric.add)); err != nil {
return nil, err
}
return o, nil
}))
v, err := c(call.This, call.Arguments...)
if err != nil {
return nil, err
}
if err = o.Set("add", rt.ToValue(bound["add"])); err != nil {
return nil, err
}
return o, nil

return v.ToObject(rt), nil
}

func (m Metric) Add(ctx context.Context, v goja.Value, addTags ...map[string]string) (bool, error) {
state := lib.GetState(ctx)
func (m Metric) add(v goja.Value, addTags ...map[string]string) (bool, error) {
state := m.core.GetState()
if state == nil {
return false, ErrMetricsAddInInitContext
}
Expand All @@ -96,28 +104,71 @@ func (m Metric) Add(ctx context.Context, v goja.Value, addTags ...map[string]str
}

sample := stats.Sample{Time: time.Now(), Metric: m.metric, Value: vfloat, Tags: stats.IntoSampleTags(&tags)}
stats.PushIfNotDone(ctx, state.Samples, sample)
stats.PushIfNotDone(m.core.GetContext(), state.Samples, sample)
return true, nil
}

type Metrics struct{}
type (
// RootModule is the root metrics module
RootModule struct{}
// ModuleInstance represents an instance of the metrics module
ModuleInstance struct {
modules.InstanceCore
}
)

var (
_ modules.IsModuleV2 = &RootModule{}
_ modules.Instance = &ModuleInstance{}
)

// NewModuleInstance implements modules.IsModuleV2 interface
func (*RootModule) NewModuleInstance(m modules.InstanceCore) modules.Instance {
return &ModuleInstance{InstanceCore: m}
}

// New returns a new RootModule.
func New() *RootModule {
return &RootModule{}
}

func New() *Metrics {
return &Metrics{}
// GetExports returns the exports of the metrics module
func (mi *ModuleInstance) GetExports() modules.Exports {
return modules.GenerateExports(mi)
}

func (*Metrics) XCounter(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Counter, isTime)
// XCounter is a counter constructor
func (mi *ModuleInstance) XCounter(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object {
v, err := mi.newMetric(call, stats.Counter)
if err != nil {
common.Throw(rt, err)
}
return v
}

func (*Metrics) XGauge(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Gauge, isTime)
// XGauge is a gauge constructor
func (mi *ModuleInstance) XGauge(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object {
v, err := mi.newMetric(call, stats.Gauge)
if err != nil {
common.Throw(rt, err)
}
return v
}

func (*Metrics) XTrend(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Trend, isTime)
// XTrend is a trend constructor
func (mi *ModuleInstance) XTrend(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object {
v, err := mi.newMetric(call, stats.Trend)
if err != nil {
common.Throw(rt, err)
}
return v
}

func (*Metrics) XRate(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Rate, isTime)
// XRate is a rate constructor
func (mi *ModuleInstance) XRate(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object {
v, err := mi.newMetric(call, stats.Rate)
if err != nil {
common.Throw(rt, err)
}
return v
}
30 changes: 20 additions & 10 deletions js/modules/k6/metrics/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/stretchr/testify/require"

"go.k6.io/k6/js/common"
"go.k6.io/k6/js/modulestest"
"go.k6.io/k6/lib"
"go.k6.io/k6/stats"
)
Expand Down Expand Up @@ -61,11 +62,14 @@ func TestMetrics(t *testing.T) {
t.Parallel()
rt := goja.New()
rt.SetFieldNameMapper(common.FieldNameMapper{})

ctxPtr := new(context.Context)
*ctxPtr = common.WithRuntime(context.Background(), rt)
rt.Set("metrics", common.Bind(rt, New(), ctxPtr))

mii := &modulestest.InstanceCore{
Runtime: rt,
InitEnv: &common.InitEnvironment{},
Ctx: context.Background(),
}
m, ok := New().NewModuleInstance(mii).(*ModuleInstance)
require.True(t, ok)
require.NoError(t, rt.Set("metrics", m.GetExports().Named))
root, _ := lib.NewGroup("", nil)
child, _ := root.Group("child")
samples := make(chan stats.SampleContainer, 1000)
Expand All @@ -84,9 +88,10 @@ func TestMetrics(t *testing.T) {
require.NoError(t, err)

t.Run("ExitInit", func(t *testing.T) {
*ctxPtr = lib.WithState(*ctxPtr, state)
mii.State = state
mii.InitEnv = nil
_, err := rt.RunString(fmt.Sprintf(`new metrics.%s("my_metric")`, fn))
assert.EqualError(t, err, "metrics must be declared in the init context at apply (native)")
assert.Contains(t, err.Error(), "metrics must be declared in the init context")
})

groups := map[string]*lib.Group{
Expand Down Expand Up @@ -175,9 +180,14 @@ func TestMetricGetName(t *testing.T) {
rt := goja.New()
rt.SetFieldNameMapper(common.FieldNameMapper{})

ctxPtr := new(context.Context)
*ctxPtr = common.WithRuntime(context.Background(), rt)
require.NoError(t, rt.Set("metrics", common.Bind(rt, New(), ctxPtr)))
mii := &modulestest.InstanceCore{
Runtime: rt,
InitEnv: &common.InitEnvironment{},
Ctx: context.Background(),
}
m, ok := New().NewModuleInstance(mii).(*ModuleInstance)
require.True(t, ok)
require.NoError(t, rt.Set("metrics", m.GetExports().Named))
v, err := rt.RunString(`
var m = new metrics.Counter("my_metric")
m.name
Expand Down
Loading

0 comments on commit 34a7743

Please sign in to comment.