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

shareable array implementation #1739

Merged
merged 10 commits into from
Jan 11, 2021
7 changes: 4 additions & 3 deletions js/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,10 @@ func (b *Bundle) instantiate(logger logrus.FieldLogger, rt *goja.Runtime, init *
// TODO: get rid of the unused ctxPtr, use a real external context (so we
// can interrupt), build the common.InitEnvironment earlier and reuse it
initenv := &common.InitEnvironment{
Logger: logger,
FileSystems: init.filesystems,
CWD: init.pwd,
SharedObjects: init.sharedObjects,
Logger: logger,
FileSystems: init.filesystems,
CWD: init.pwd,
}
ctx := common.WithInitEnv(context.Background(), initenv)
*init.ctxPtr = common.WithRuntime(ctx, rt)
Expand Down
33 changes: 33 additions & 0 deletions js/common/initenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package common
import (
"net/url"
"path/filepath"
"sync"

"github.com/sirupsen/logrus"
"github.com/spf13/afero"
Expand All @@ -37,6 +38,7 @@ type InitEnvironment struct {
// TODO: add RuntimeOptions and other properties, goja sources, etc.
// ideally, we should leave this as the only data structure necessary for
// executing the init context for all JS modules
SharedObjects *SharedObjects
}

// GetAbsFilePath should be used to access the FileSystems, since afero has a
Expand All @@ -60,3 +62,34 @@ func (ie *InitEnvironment) GetAbsFilePath(filename string) string {
}
return filename
}

// SharedObjects is a collection of general store for objects to be shared. It is mostly a wrapper
// around map[string]interface with a lock and stuff.
// The reason behind not just using sync.Map is that it still needs a lock when we want to only call
// the function constructor if there is no such key at which point you already need a lock so ...
type SharedObjects struct {
data map[string]interface{}
l sync.Mutex
}

// NewSharedObjects returns a new SharedObjects ready to use
func NewSharedObjects() *SharedObjects {
return &SharedObjects{
data: make(map[string]interface{}),
}
}

// GetOrCreateShare returns a shared value with the given name or sets it's value whatever
// createCallback returns and returns it.
func (so *SharedObjects) GetOrCreateShare(name string, createCallback func() interface{}) interface{} {
so.l.Lock()
defer so.l.Unlock()

value, ok := so.data[name]
if !ok {
value = createCallback()
so.data[name] = value
}

return value
}
5 changes: 4 additions & 1 deletion js/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,20 @@ func getSimpleRunner(tb testing.TB, filename, data string, opts ...interface{})
var (
fs = afero.NewMemMapFs()
rtOpts = lib.RuntimeOptions{CompatibilityMode: null.NewString("base", true)}
logger = testutils.NewLogger(tb)
)
for _, o := range opts {
switch opt := o.(type) {
case afero.Fs:
fs = opt
case lib.RuntimeOptions:
rtOpts = opt
case *logrus.Logger:
logger = opt
}
}
return New(
testutils.NewLogger(tb),
logger,
&loader.SourceData{
URL: &url.URL{Path: filename, Scheme: "file"},
Data: []byte(data),
Expand Down
5 changes: 4 additions & 1 deletion js/initcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ type InitContext struct {
compatibilityMode lib.CompatibilityMode

logger logrus.FieldLogger

sharedObjects *common.SharedObjects
mstoykov marked this conversation as resolved.
Show resolved Hide resolved
}

// NewInitContext creates a new initcontext with the provided arguments
Expand All @@ -85,6 +87,7 @@ func NewInitContext(
programs: make(map[string]programWithSource),
compatibilityMode: compatMode,
logger: logger,
sharedObjects: common.NewSharedObjects(),
}
}

Expand All @@ -110,6 +113,7 @@ func newBoundInitContext(base *InitContext, ctxPtr *context.Context, rt *goja.Ru
programs: programs,
compatibilityMode: base.compatibilityMode,
logger: base.logger,
sharedObjects: base.sharedObjects,
}
}

Expand Down Expand Up @@ -162,7 +166,6 @@ func (i *InitContext) requireFile(name string) (goja.Value, error) {

if pgm.pgm == nil {
// Load the sources; the loader takes care of remote loading, etc.
// TODO: don't use the Global logger
data, err := loader.Load(i.logger, i.filesystems, fileURL, name)
if err != nil {
return goja.Undefined(), err
Expand Down
1 change: 1 addition & 0 deletions js/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
_ "github.com/loadimpact/k6/js/modules/k6"
_ "github.com/loadimpact/k6/js/modules/k6/crypto"
_ "github.com/loadimpact/k6/js/modules/k6/crypto/x509"
_ "github.com/loadimpact/k6/js/modules/k6/data"
_ "github.com/loadimpact/k6/js/modules/k6/encoding"
_ "github.com/loadimpact/k6/js/modules/k6/grpc"
_ "github.com/loadimpact/k6/js/modules/k6/http"
Expand Down
76 changes: 76 additions & 0 deletions js/modules/k6/data/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package data
na-- marked this conversation as resolved.
Show resolved Hide resolved

import (
"context"
"encoding/json"
"reflect"

"github.com/dop251/goja"
"github.com/loadimpact/k6/js/common"
"github.com/loadimpact/k6/js/internal/modules"
"github.com/loadimpact/k6/lib"
"github.com/pkg/errors"
)

type data struct{}

func init() {
modules.Register("k6/data", new())
}

func new() *data {
return &data{}
}

const sharedArrayNamePrefix = "k6/data/SharedArray."

// XSharedArray is a constructor returning a shareable read-only array
// indentified by the name and having their contents be whatever the call returns
func (d *data) XSharedArray(ctx context.Context, name string, call goja.Callable) (goja.Value, error) {
if lib.GetState(ctx) != nil {
return nil, errors.New("new SharedArray must be called in the init context")
}

initEnv := common.GetInitEnv(ctx)
if initEnv == nil {
return nil, errors.New("missing init environment")
}

name = sharedArrayNamePrefix + name
na-- marked this conversation as resolved.
Show resolved Hide resolved
value := initEnv.SharedObjects.GetOrCreateShare(name, func() interface{} {
return getShareArrayFromCall(common.GetRuntime(ctx), call)
})
array, ok := value.(sharedArray)
if !ok { // TODO more info in the error?
return nil, errors.New("wrong type of shared object")
}

return array.wrap(&ctx, common.GetRuntime(ctx)), nil
}

func getShareArrayFromCall(rt *goja.Runtime, call goja.Callable) sharedArray {
gojaValue, err := call(goja.Undefined())
if err != nil {
common.Throw(rt, err)
}
// TODO this can probably be better handled
if gojaValue.ExportType().Kind() != reflect.Slice {
common.Throw(rt, errors.New("only arrays can be made into SharedArray")) // TODO better error
}

// TODO this can probably be done better if we just iterate over the internal array, but ...
// that might be a bit harder given what currently goja provides
var tmpArr []interface{}
if err = rt.ExportTo(gojaValue, &tmpArr); err != nil {
common.Throw(rt, err)
}

arr := make([][]byte, len(tmpArr))
for index := range arr {
arr[index], err = json.Marshal(tmpArr[index])
if err != nil {
common.Throw(rt, err)
}
}
return sharedArray{arr: arr}
}
108 changes: 108 additions & 0 deletions js/modules/k6/data/share.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
*
* k6 - a next-generation load testing tool
* Copyright (C) 2020 Load Impact
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package data

import (
"context"
"encoding/json"

"github.com/dop251/goja"
"github.com/loadimpact/k6/js/common"
)

// TODO fix it not working really well with setupData or just make it more broken
// TODO fix it working with console.log
type sharedArray struct {
arr [][]byte
}

func (s sharedArray) wrap(ctxPtr *context.Context, rt *goja.Runtime) goja.Value {
cal, err := rt.RunString(arrayWrapperCode)
if err != nil {
common.Throw(rt, err)
}
call, _ := goja.AssertFunction(cal)
wrapped, err := call(goja.Undefined(), rt.ToValue(common.Bind(rt, s, ctxPtr)))
if err != nil {
common.Throw(rt, err)
}

return wrapped
}

func (s sharedArray) Get(index int) (interface{}, error) {
if index < 0 || index >= len(s.arr) {
return goja.Undefined(), nil
}

var tmp interface{}
if err := json.Unmarshal(s.arr[index], &tmp); err != nil {
return goja.Undefined(), err
}
return tmp, nil
}

func (s sharedArray) Length() int {
return len(s.arr)
}

type sharedArrayIterator struct {
a *sharedArray
index int
}

func (sai *sharedArrayIterator) Next() (interface{}, error) {
if sai.index == len(sai.a.arr)-1 {
return map[string]bool{"done": true}, nil
}
sai.index++
var tmp interface{}
if err := json.Unmarshal(sai.a.arr[sai.index], &tmp); err != nil {
return goja.Undefined(), err
}
return map[string]interface{}{"value": tmp}, nil
}

func (s sharedArray) Iterator() *sharedArrayIterator {
return &sharedArrayIterator{a: &s, index: -1}
}

const arrayWrapperCode = `(function(val) {
var arrayHandler = {
get: function(target, property, receiver) {
switch (property){
case "length":
return target.length();
case Symbol.iterator:
return function() {
return target.iterator();
};
}
var i = parseInt(property);
if (isNaN(i)) {
return undefined;
}

return target.get(i);
}
};
return new Proxy(val, arrayHandler);
})`
Loading