Skip to content

Commit

Permalink
PoC shareable array implementation
Browse files Browse the repository at this point in the history
This is part of #532

The reason behind this not redefing ... how to open a file or load data
from one is that, as in the majority of the cases after that there is
some amount of ... transformation of the data, so that won't help much.

The original idea was to have a shareable JSON ... but the more concrete
array implementation is faster (in some cases by a lot) and also seems
like a better fix for the first iteration of this, as that will be the
majority of the data type. So it is very likely that the non array parts
will be dropped in favor of doing them later and this getting merged
faster.

There are still more things to be done and depending on whether or not
we want to support iterating (with for-of) it can probably be done
without some of the js code.
  • Loading branch information
mstoykov committed Nov 25, 2020
1 parent 212cc57 commit 4cf1ba8
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 1 deletion.
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
97 changes: 97 additions & 0 deletions js/initcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ package js

import (
"context"
"encoding/json"
"fmt"
"net/url"
"path/filepath"
"reflect"
"strings"
"sync"

"github.com/dop251/goja"
"github.com/pkg/errors"
Expand Down Expand Up @@ -69,6 +72,9 @@ type InitContext struct {
compatibilityMode lib.CompatibilityMode

logger logrus.FieldLogger

shares map[string]interface{}
sharesLock *sync.Mutex
}

// NewInitContext creates a new initcontext with the provided arguments
Expand All @@ -85,6 +91,9 @@ func NewInitContext(
programs: make(map[string]programWithSource),
compatibilityMode: compatMode,
logger: logger,

shares: make(map[string]interface{}),
sharesLock: new(sync.Mutex),
}
}

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

shares: base.shares,
sharesLock: base.sharesLock,
}
}

// NewShare ...
// TODO rename
func (i *InitContext) NewShare(ctx context.Context, name string, call goja.Callable) goja.Value {
i.sharesLock.Lock()
defer i.sharesLock.Unlock()
value, ok := i.shares[name]
rt := common.GetRuntime(ctx)
if !ok { //nolint:nestif
gojaValue, err := call(goja.Undefined())
if err != nil {
common.Throw(rt, err)
}
if gojaValue.ExportType().Kind() == reflect.Slice {
// fmt.Println("it's a slice")
var tmpArr []interface{}
if err = rt.ExportTo(gojaValue, &tmpArr); err != nil {
common.Throw(rt, err)
}

arr := make([][]byte, len(tmpArr))
for i := range arr {
arr[i], err = json.Marshal(tmpArr[i])
if err != nil {
common.Throw(rt, err)
}
}
value = sharedArray{arr: arr}
} else {
// fmt.Println("it's an object")
value = shared{gojaValue.ToObject(rt)}
}
i.shares[name] = value
}
switch value.(type) {
case sharedArray:
// fmt.Println("wrapping array/slice")
// TODO cache this
cal, err := rt.RunString(`(function(val) {
var arrayHandler = {
get: function(target, property, receiver) {
// console.log("accessing ", property)
switch (property){
case "length":
return target.length()
case Symbol.iterator:
return function() {return target.iterator()}
/*
return function(){
var index = 0;
return {
"next": function() {
if (index >= target.length()) {
return {done: true}
}
var result = {value:target.get(index)};
index++;
return result;
}
}
}
*/
}
return target.get(property);
}
};
return new Proxy(val, arrayHandler)
})`)
if err != nil {
common.Throw(rt, err)
}
call, _ := goja.AssertFunction(cal)
wrapped, err := call(goja.Undefined(), i.runtime.ToValue(common.Bind(i.runtime, value, i.ctxPtr)))
if err != nil {
common.Throw(rt, err)
}

return wrapped
case shared:
// TODO
}

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

// Require is called when a module/file needs to be loaded by a script
Expand Down
99 changes: 99 additions & 0 deletions js/share.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
*
* 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 js

// TODO move this to another package
// it can possibly be even in a separate repo if the error handling is fixed

import (
"context"
"encoding/json"

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

// TODO rename
// TODO check how it works with setup data
// TODO check how it works if logged
// TODO maybe drop it and leave the sharedArray only for now
type shared struct {
value *goja.Object
}

func (s shared) Get(ctx context.Context, index goja.Value) (interface{}, error) {
rt := common.GetRuntime(ctx)
// TODO other index
val := s.value.Get(index.String())
if val == nil {
return goja.Undefined(), nil
}
b, err := val.ToObject(rt).MarshalJSON()
if err != nil { // cache bytes, pre marshal
return goja.Undefined(), err
}
var tmp interface{}
if err = json.Unmarshal(b, &tmp); err != nil {
return goja.Undefined(), err
}
return tmp, nil
}

type sharedArray struct {
arr [][]byte
}

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}
}
107 changes: 107 additions & 0 deletions js/share_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
*
* 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 js

import (
"context"
"io/ioutil"
"testing"

"github.com/loadimpact/k6/lib"
"github.com/loadimpact/k6/lib/testutils"
"github.com/loadimpact/k6/stats"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestInitContextNewShareable(t *testing.T) {
data := `
function generateArray() {
console.log("once");
var n = 50;
var arr = new Array(n);
for (var i = 0 ; i <n; i++) {
arr[i] = "something" +i;
}
return arr;
}
var s = newShare("something", generateArray);
exports.default = function() {
if (s[2] !== "something2") {
throw new Error("bad s[2]="+s[2])
}
if (s.length != 50) {
throw new Error("bad length " +_s.length)
}
var i = 0;
for (var v of s) {
if (v !== "something"+i) {
throw new Error("bad v="+v+" for i="+i)
}
i++;
}
}`

logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
logger.Out = ioutil.Discard
hook := testutils.SimpleLogrusHook{
HookedLevels: []logrus.Level{logrus.InfoLevel, logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel},
}
logger.AddHook(&hook)

r1, err := getSimpleRunner(t, "/script.js", data, logger)
require.NoError(t, err)
entries := hook.Drain()
require.Len(t, entries, 1)
assert.Equal(t, logrus.InfoLevel, entries[0].Level)
assert.Equal(t, "once", entries[0].Message)

r2, err := NewFromArchive(logger, r1.MakeArchive(), lib.RuntimeOptions{})
require.NoError(t, err)
entries = hook.Drain()
require.Len(t, entries, 1)
assert.Equal(t, logrus.InfoLevel, entries[0].Level)
assert.Equal(t, "once", entries[0].Message)

testdata := map[string]*Runner{"Source": r1, "Archive": r2}
for name, r := range testdata {
t.Parallel()
r := r
t.Run(name, func(t *testing.T) {
samples := make(chan stats.SampleContainer, 100)
initVU, err := r.NewVU(1, samples)
if assert.NoError(t, err) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
vu := initVU.Activate(&lib.VUActivationParams{RunContext: ctx})
err := vu.RunOnce()
assert.NoError(t, err)
entries := hook.Drain()
require.Len(t, entries, 0)
}
})
}
}

0 comments on commit 4cf1ba8

Please sign in to comment.