mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
remove creator/bagent/bspawn
This commit is contained in:
parent
64db5e0f23
commit
77372fe3cc
31 changed files with 0 additions and 2825 deletions
|
|
@ -1,30 +0,0 @@
|
|||
The following applies to the files errgroup.go and errgroup_test.go
|
||||
in the same directory this file resides:
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package bench
|
||||
|
||||
import "context"
|
||||
|
||||
// Benchmark is an interface to guide the creation of new pilosa benchmarks or
|
||||
// benchmark components. It defines 2 methods, Init, and Run. These are separate
|
||||
// methods so that benchmark running code can time only the running of the
|
||||
// benchmark, and not any setup.
|
||||
type Benchmark interface {
|
||||
// Init takes a list of hosts and an agent number. It is generally expected
|
||||
// to set up a connection to pilosa using whatever client it chooses. The
|
||||
// agentNum should be used to parameterize the benchmark's configuration if
|
||||
// it is being run simultaneously on multiple "agents". E.G. the agentNum
|
||||
// might be used to make a random seed different for each agent, or have
|
||||
// each agent set a different set of bits. Init's doc string should document
|
||||
// how the agentNum affects it. Every benchmark should have a 'Name' field
|
||||
// set by init, which appears when the benchmark is marshalled to json as
|
||||
// "name".
|
||||
Init(hosts []string, agentNum int) error
|
||||
|
||||
// Run runs the benchmark. The return value of Run is kept generic so that
|
||||
// any relevant statistics or metrics that may be specific to the benchmark
|
||||
// in question can be reported. TODO guidelines for what gets included in
|
||||
// results and what will get added by other stuff. Run does not need to
|
||||
// report total run time in `results`, as that will be added by calling
|
||||
// code.
|
||||
Run(ctx context.Context) map[string]interface{}
|
||||
}
|
||||
|
||||
// Command extends Benchmark by adding methods for configuring via command line flags and returning usage information.
|
||||
type Command interface {
|
||||
Benchmark
|
||||
|
||||
// ConsumeFlags sets and parses flags, and then returns flagSet.Args(). This
|
||||
// is so that multiple benchmarks can be specified at the command line.
|
||||
ConsumeFlags(args []string) ([]string, error)
|
||||
|
||||
// Usage returns information on how to use this benchmark. The usage string
|
||||
// should explain how the agent num affects the benchmark's operation.
|
||||
Usage() string
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
package bench
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pilosa/pilosa"
|
||||
)
|
||||
|
||||
func firstHostClient(hosts []string) (*pilosa.Client, error) {
|
||||
client, err := pilosa.NewClient(hosts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func roundRobinClient(hosts []string, agentNum int) (*pilosa.Client, error) {
|
||||
clientNum := agentNum % len(hosts)
|
||||
return firstHostClient(hosts[clientNum:])
|
||||
}
|
||||
|
||||
// HasClient provides a reusable component for Benchmark implementations which
|
||||
// provides the Init method, a ClientType argument and a cli internal variable.
|
||||
type HasClient struct {
|
||||
client *pilosa.Client
|
||||
ClientType string `json:"client-type"`
|
||||
ContentType string `json:"content-type"`
|
||||
}
|
||||
|
||||
// Init for HasClient looks at the ClientType field and creates a pilosa client
|
||||
// either using the first host in the list of hosts or based on the agent
|
||||
// number mod len(hosts)
|
||||
func (h *HasClient) Init(hosts []string, agentNum int) error {
|
||||
var err error
|
||||
switch h.ClientType {
|
||||
case "single":
|
||||
h.client, err = firstHostClient(hosts)
|
||||
case "round_robin":
|
||||
h.client, err = roundRobinClient(hosts, agentNum)
|
||||
default:
|
||||
err = fmt.Errorf("Unsupported ClientType: %v", h.ClientType)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch h.ContentType {
|
||||
case "protobuf":
|
||||
return nil
|
||||
case "pql":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("Unsupported ContentType: %v", h.ContentType)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HasClient) ExecuteQuery(contentType, db, query string, ctx context.Context) (interface{}, error) {
|
||||
if contentType == "protobuf" {
|
||||
return h.client.ExecuteQuery(ctx, db, query, true)
|
||||
} else if contentType == "pql" {
|
||||
return h.client.ExecutePQL(ctx, db, query)
|
||||
} else {
|
||||
return nil, errors.New("unsupport content type")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
package bench
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DiagonalSetBits sets bits with increasing profile id and bitmap id.
|
||||
type DiagonalSetBits struct {
|
||||
HasClient
|
||||
Name string `json:"name"`
|
||||
BaseBitmapID int `json:"base-bitmap-id"`
|
||||
BaseProfileID int `json:"base-profile-id"`
|
||||
Iterations int `json:"iterations"`
|
||||
DB string `json:"db"`
|
||||
}
|
||||
|
||||
// Init sets up the pilosa client and modifies the configured values based on
|
||||
// the agent num.
|
||||
func (b *DiagonalSetBits) Init(hosts []string, agentNum int) error {
|
||||
b.Name = "diagonal-set-bits"
|
||||
b.BaseBitmapID = b.BaseBitmapID + (agentNum * b.Iterations)
|
||||
b.BaseProfileID = b.BaseProfileID + (agentNum * b.Iterations)
|
||||
return b.HasClient.Init(hosts, agentNum)
|
||||
}
|
||||
|
||||
// Usage returns the usage message to be printed.
|
||||
func (b *DiagonalSetBits) Usage() string {
|
||||
return `
|
||||
diagonal-set-bits sets bits with increasing profile id and bitmap id.
|
||||
|
||||
Agent num offsets both the base profile id and base bitmap id by the number of
|
||||
iterations, so that only bits on the main diagonal are set, and agents don't
|
||||
overlap at all.
|
||||
|
||||
Usage: diagonal-set-bits [arguments]
|
||||
|
||||
The following arguments are available:
|
||||
|
||||
-base-bitmap-id int
|
||||
bits being set will all be greater than BaseBitmapID
|
||||
|
||||
-base-profile-id int
|
||||
profile id num to start from
|
||||
|
||||
-iterations int
|
||||
number of bits to set
|
||||
|
||||
-db string
|
||||
pilosa db to use
|
||||
|
||||
-client-type string
|
||||
Can be 'single' (all agents hitting one host) or 'round_robin'
|
||||
|
||||
-content-type string
|
||||
protobuf or pql
|
||||
`[1:]
|
||||
}
|
||||
|
||||
// ConsumeFlags parses all flags up to the next non flag argument (argument does
|
||||
// not start with "-" and isn't the value of a flag). It returns the remaining
|
||||
// args.
|
||||
func (b *DiagonalSetBits) ConsumeFlags(args []string) ([]string, error) {
|
||||
fs := flag.NewFlagSet("DiagonalSetBits", flag.ContinueOnError)
|
||||
fs.SetOutput(ioutil.Discard)
|
||||
fs.IntVar(&b.BaseBitmapID, "base-bitmap-id", 0, "")
|
||||
fs.IntVar(&b.BaseProfileID, "base-profile-id", 0, "")
|
||||
fs.IntVar(&b.Iterations, "iterations", 100, "")
|
||||
fs.StringVar(&b.DB, "db", "benchdb", "")
|
||||
fs.StringVar(&b.ClientType, "client-type", "single", "")
|
||||
fs.StringVar(&b.ContentType, "content-type", "protobuf", "")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.Args(), nil
|
||||
}
|
||||
|
||||
// Run runs the DiagonalSetBits benchmark
|
||||
func (b *DiagonalSetBits) Run(ctx context.Context) map[string]interface{} {
|
||||
results := make(map[string]interface{})
|
||||
if b.client == nil {
|
||||
results["error"] = fmt.Errorf("No client set for DiagonalSetBits")
|
||||
return results
|
||||
}
|
||||
s := NewStats()
|
||||
var start time.Time
|
||||
for n := 0; n < b.Iterations; n++ {
|
||||
query := fmt.Sprintf("SetBit(%d, 'frame.n', %d)", b.BaseBitmapID+n, b.BaseProfileID+n)
|
||||
start = time.Now()
|
||||
_, err := b.client.ExecuteQuery(ctx, b.DB, query, true)
|
||||
if err != nil {
|
||||
results["error"] = err
|
||||
return results
|
||||
}
|
||||
s.Add(time.Now().Sub(start))
|
||||
}
|
||||
AddToResults(s, results)
|
||||
return results
|
||||
}
|
||||
39
bench/doc.go
39
bench/doc.go
|
|
@ -1,39 +0,0 @@
|
|||
// bench contains benchmarks and common utilities useful to benchmarks
|
||||
//
|
||||
// In order to write new benchmarks, one must satisfy the Benchmark and Command
|
||||
// interfaces in bench.go. In order to use the benchmark from pilosactl, it
|
||||
// needs to be wired in in two places. The first is BagentCommand.ParseFlags,
|
||||
// where a case statement needs to be added, and the second is just adding the
|
||||
// benchmark to the BagentCommand.Usage usage string.
|
||||
//
|
||||
// When writing a new benchmark, there are a few things to keep in mind other
|
||||
// than just implementing the interface:
|
||||
//
|
||||
// 1. The benchmark should modify its own configuration in its Init method based
|
||||
// on the agentNum it is given. How it modifies is specific to the benchmark,
|
||||
// but the idea is that it should make sense to call the benchmark with the same
|
||||
// configuration, but multiple different agent numbers, and it should do useful
|
||||
// work each time (i.e. not just setting the same bits, or running the same
|
||||
// queries).
|
||||
//
|
||||
// 2. The Init method should do everything that needs to be done to get the
|
||||
// benchmark to a runnable state - all code in run should be the stuff that we
|
||||
// actually want to time.
|
||||
//
|
||||
// 3. The Run method does not need to report the total runtime - that is collected
|
||||
// by calling code.
|
||||
//
|
||||
// 4. Usage should follow the format in other benchmarks, and explain how the
|
||||
// benchmark uses agentNum to modify its behavior
|
||||
//
|
||||
//
|
||||
// Files:
|
||||
//
|
||||
// 1. client.go contains pilosa client code which is shared by many benchmarks
|
||||
//
|
||||
// 2. errgroup.go contains the ErrGroup implementation copied from golang.org/x/
|
||||
// so as not to pull in a bunch of useless deps.
|
||||
//
|
||||
// 3. stats.go contains useful code for gathering stats about a series of timed
|
||||
// operations.
|
||||
package bench
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package errgroup provides synchronization, error propagation, and Context
|
||||
// cancelation for groups of goroutines working on subtasks of a common task.
|
||||
package bench
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"context"
|
||||
)
|
||||
|
||||
// An ErrGroup is a collection of goroutines working on subtasks that are part of
|
||||
// the same overall task.
|
||||
//
|
||||
// A zero ErrGroup is valid and does not cancel on error.
|
||||
type ErrGroup struct {
|
||||
cancel func()
|
||||
|
||||
wg sync.WaitGroup
|
||||
|
||||
errOnce sync.Once
|
||||
err error
|
||||
}
|
||||
|
||||
// WithContext returns a new ErrGroup and an associated Context derived from ctx.
|
||||
//
|
||||
// The derived Context is canceled the first time a function passed to Go
|
||||
// returns a non-nil error or the first time Wait returns, whichever occurs
|
||||
// first.
|
||||
func WithContext(ctx context.Context) (*ErrGroup, context.Context) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &ErrGroup{cancel: cancel}, ctx
|
||||
}
|
||||
|
||||
// Wait blocks until all function calls from the Go method have returned, then
|
||||
// returns the first non-nil error (if any) from them.
|
||||
func (g *ErrGroup) Wait() error {
|
||||
g.wg.Wait()
|
||||
if g.cancel != nil {
|
||||
g.cancel()
|
||||
}
|
||||
return g.err
|
||||
}
|
||||
|
||||
// Go calls the given function in a new goroutine.
|
||||
//
|
||||
// The first call to return a non-nil error cancels the group; its error will be
|
||||
// returned by Wait.
|
||||
func (g *ErrGroup) Go(f func() error) {
|
||||
g.wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer g.wg.Done()
|
||||
|
||||
if err := f(); err != nil {
|
||||
g.errOnce.Do(func() {
|
||||
g.err = err
|
||||
if g.cancel != nil {
|
||||
g.cancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bench_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"context"
|
||||
"github.com/pilosa/pilosa/bench"
|
||||
)
|
||||
|
||||
var (
|
||||
Web = fakeSearch("web")
|
||||
Image = fakeSearch("image")
|
||||
Video = fakeSearch("video")
|
||||
)
|
||||
|
||||
type Result string
|
||||
type Search func(ctx context.Context, query string) (Result, error)
|
||||
|
||||
func fakeSearch(kind string) Search {
|
||||
return func(_ context.Context, query string) (Result, error) {
|
||||
return Result(fmt.Sprintf("%s result for %q", kind, query)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// JustErrors illustrates the use of a ErrGroup in place of a sync.WaitGroup to
|
||||
// simplify goroutine counting and error handling. This example is derived from
|
||||
// the sync.WaitGroup example at https://golang.org/pkg/sync/#example_WaitGroup.
|
||||
func ExampleGroup_justErrors() {
|
||||
var g bench.ErrGroup
|
||||
var urls = []string{
|
||||
"http://www.golang.org/",
|
||||
"http://www.google.com/",
|
||||
"http://www.somestupidname.com/",
|
||||
}
|
||||
for _, url := range urls {
|
||||
// Launch a goroutine to fetch the URL.
|
||||
url := url // https://golang.org/doc/faq#closures_and_goroutines
|
||||
g.Go(func() error {
|
||||
// Fetch the URL.
|
||||
resp, err := http.Get(url)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
// Wait for all HTTP fetches to complete.
|
||||
if err := g.Wait(); err == nil {
|
||||
fmt.Println("Successfully fetched all URLs.")
|
||||
}
|
||||
}
|
||||
|
||||
// Parallel illustrates the use of a ErrGroup for synchronizing a simple parallel
|
||||
// task: the "Google Search 2.0" function from
|
||||
// https://talks.golang.org/2012/concurrency.slide#46, augmented with a Context
|
||||
// and error-handling.
|
||||
func ExampleGroup_parallel() {
|
||||
Google := func(ctx context.Context, query string) ([]Result, error) {
|
||||
g, ctx := bench.WithContext(ctx)
|
||||
|
||||
searches := []Search{Web, Image, Video}
|
||||
results := make([]Result, len(searches))
|
||||
for i, search := range searches {
|
||||
i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines
|
||||
g.Go(func() error {
|
||||
result, err := search(ctx, query)
|
||||
if err == nil {
|
||||
results[i] = result
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
results, err := Google(context.Background(), "golang")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// web result for "golang"
|
||||
// image result for "golang"
|
||||
// video result for "golang"
|
||||
}
|
||||
|
||||
func TestZeroGroup(t *testing.T) {
|
||||
err1 := errors.New("errgroup_test: 1")
|
||||
err2 := errors.New("errgroup_test: 2")
|
||||
|
||||
cases := []struct {
|
||||
errs []error
|
||||
}{
|
||||
{errs: []error{}},
|
||||
{errs: []error{nil}},
|
||||
{errs: []error{err1}},
|
||||
{errs: []error{err1, nil}},
|
||||
{errs: []error{err1, nil, err2}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
var g bench.ErrGroup
|
||||
|
||||
var firstErr error
|
||||
for i, err := range tc.errs {
|
||||
err := err
|
||||
g.Go(func() error { return err })
|
||||
|
||||
if firstErr == nil && err != nil {
|
||||
firstErr = err
|
||||
}
|
||||
|
||||
if gErr := g.Wait(); gErr != firstErr {
|
||||
t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+
|
||||
"g.Wait() = %v; want %v",
|
||||
g, tc.errs[:i+1], err, firstErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithContext(t *testing.T) {
|
||||
errDoom := errors.New("group_test: doomed")
|
||||
|
||||
cases := []struct {
|
||||
errs []error
|
||||
want error
|
||||
}{
|
||||
{want: nil},
|
||||
{errs: []error{nil}, want: nil},
|
||||
{errs: []error{errDoom}, want: errDoom},
|
||||
{errs: []error{errDoom, nil}, want: errDoom},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
g, ctx := bench.WithContext(context.Background())
|
||||
|
||||
for _, err := range tc.errs {
|
||||
err := err
|
||||
g.Go(func() error { return err })
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != tc.want {
|
||||
t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+
|
||||
"g.Wait() = %v; want %v",
|
||||
g, tc.errs, err, tc.want)
|
||||
}
|
||||
|
||||
canceled := false
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
canceled = true
|
||||
default:
|
||||
}
|
||||
if !canceled {
|
||||
t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+
|
||||
"ctx.Done() was not closed",
|
||||
g, tc.errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
203
bench/import.go
203
bench/import.go
|
|
@ -1,203 +0,0 @@
|
|||
package bench
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
|
||||
"sort"
|
||||
|
||||
"github.com/pilosa/pilosa/pilosactl"
|
||||
)
|
||||
|
||||
// NewImport returns an Import Benchmark which pilosactl importer configured.
|
||||
func NewImport(stdin io.Reader, stdout, stderr io.Writer) *Import {
|
||||
return &Import{
|
||||
ImportCommand: pilosactl.NewImportCommand(stdin, stdout, stderr),
|
||||
}
|
||||
}
|
||||
|
||||
// Import sets bits with increasing profile id and bitmap id.
|
||||
type Import struct {
|
||||
Name string `json:"name"`
|
||||
BaseBitmapID int64 `json:"base-bitmap-id"`
|
||||
MaxBitmapID int64 `json:"max-bitmap-id"`
|
||||
BaseProfileID int64 `json:"base-profile-id"`
|
||||
MaxProfileID int64 `json:"max-profile-id"`
|
||||
RandomBitmapOrder bool `json:"random-bitmap-order"`
|
||||
MinBitsPerMap int64 `json:"min-bits-per-map"`
|
||||
MaxBitsPerMap int64 `json:"max-bits-per-map"`
|
||||
AgentControls string `json:"agent-controls"`
|
||||
Seed int64 `json:"seed"`
|
||||
numbits int
|
||||
|
||||
*pilosactl.ImportCommand
|
||||
}
|
||||
|
||||
// Usage returns the usage message to be printed.
|
||||
func (b *Import) Usage() string {
|
||||
return `
|
||||
import generates an import file and imports using pilosa's bulk import interface
|
||||
|
||||
Agent num can have various effects - see -agent-controls flag.
|
||||
|
||||
Usage: import [arguments]
|
||||
|
||||
The following arguments are available:
|
||||
|
||||
-base-bitmap-id int
|
||||
bits being set will all be greater than this
|
||||
|
||||
-max-bitmap-id int
|
||||
bits being set will all be less than this
|
||||
|
||||
-base-profile-id int
|
||||
profile id num to start from
|
||||
|
||||
-max-profile-id int
|
||||
maximum profile id to generate
|
||||
|
||||
-random-bitmap-order
|
||||
if this option is set, the import file will not be sorted by bitmap id
|
||||
|
||||
-min-bits-per-map int
|
||||
minimum number of bits set per bitmap
|
||||
|
||||
-max-bits-per-map int
|
||||
maximum number of bits set per bitmap
|
||||
|
||||
-agent-controls string
|
||||
can be 'height', 'width', or empty (TODO or square?)- increasing
|
||||
number of agents modulates bitmap id range, profile id range,
|
||||
or just sets more bits in the same range.
|
||||
|
||||
-seed int
|
||||
seed for RNG
|
||||
|
||||
-db string
|
||||
pilosa db to use
|
||||
|
||||
-frame string
|
||||
frame to import into
|
||||
`[1:]
|
||||
}
|
||||
|
||||
// ConsumeFlags parses all flags up to the next non flag argument (argument does
|
||||
// not start with "-" and isn't the value of a flag). It returns the remaining
|
||||
// args.
|
||||
func (b *Import) ConsumeFlags(args []string) ([]string, error) {
|
||||
fs := flag.NewFlagSet("Import", flag.ContinueOnError)
|
||||
fs.SetOutput(ioutil.Discard)
|
||||
fs.Int64Var(&b.BaseBitmapID, "base-bitmap-id", 0, "")
|
||||
fs.Int64Var(&b.MaxBitmapID, "max-bitmap-id", 1000, "")
|
||||
fs.Int64Var(&b.BaseProfileID, "base-profile-id", 0, "")
|
||||
fs.Int64Var(&b.MaxProfileID, "max-profile-id", 1000, "")
|
||||
fs.BoolVar(&b.RandomBitmapOrder, "random-bitmap-order", false, "")
|
||||
fs.Int64Var(&b.MinBitsPerMap, "min-bits-per-map", 0, "")
|
||||
fs.Int64Var(&b.MaxBitsPerMap, "max-bits-per-map", 10, "")
|
||||
fs.StringVar(&b.AgentControls, "agent-controls", "", "")
|
||||
fs.Int64Var(&b.Seed, "seed", 0, "")
|
||||
fs.StringVar(&b.Database, "db", "benchdb", "")
|
||||
fs.StringVar(&b.Frame, "frame", "testframe", "")
|
||||
fs.IntVar(&b.BufferSize, "buffer-size", 10000000, "")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.Args(), nil
|
||||
}
|
||||
|
||||
// Init generates import data based on the agent num and fields of 'b'.
|
||||
func (b *Import) Init(hosts []string, agentNum int) error {
|
||||
if len(hosts) == 0 {
|
||||
return fmt.Errorf("Need at least one host")
|
||||
}
|
||||
b.Name = "import"
|
||||
b.Host = hosts[0]
|
||||
// generate csv data
|
||||
b.Seed = b.Seed + int64(agentNum)
|
||||
switch b.AgentControls {
|
||||
case "height":
|
||||
numBitmapIDs := (b.MaxBitmapID - b.BaseBitmapID)
|
||||
b.BaseBitmapID = b.BaseBitmapID + (numBitmapIDs * int64(agentNum))
|
||||
b.MaxBitmapID = b.BaseBitmapID + numBitmapIDs
|
||||
case "width":
|
||||
numProfileIDs := (b.MaxProfileID - b.BaseProfileID)
|
||||
b.BaseProfileID = b.BaseProfileID + (numProfileIDs * int64(agentNum))
|
||||
b.MaxProfileID = b.BaseProfileID + numProfileIDs
|
||||
case "":
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("agent-controls: '%v' is not supported", b.AgentControls)
|
||||
}
|
||||
f, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// set b.Paths)
|
||||
num := GenerateImportCSV(f, b.BaseBitmapID, b.MaxBitmapID, b.BaseProfileID, b.MaxProfileID,
|
||||
b.MinBitsPerMap, b.MaxBitsPerMap, b.Seed, b.RandomBitmapOrder)
|
||||
b.numbits = num
|
||||
// set b.Paths
|
||||
b.Paths = []string{f.Name()}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// Run runs the Import benchmark
|
||||
func (b *Import) Run(ctx context.Context) map[string]interface{} {
|
||||
results := make(map[string]interface{})
|
||||
results["numbits"] = b.numbits
|
||||
results["db"] = b.Database
|
||||
err := b.ImportCommand.Run(ctx)
|
||||
|
||||
if err != nil {
|
||||
results["error"] = err.Error()
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Int64Slice is a sortable slice of 64 bit signed ints
|
||||
type Int64Slice []int64
|
||||
|
||||
func (s Int64Slice) Len() int { return len(s) }
|
||||
func (s Int64Slice) Less(i, j int) bool { return s[i] < s[j] }
|
||||
func (s Int64Slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
// GenerateImportCSV writes a generated csv to 'w' which is in the form pilosactl expects for imports.
|
||||
func GenerateImportCSV(w io.Writer, baseBitmapID, maxBitmapID, baseProfileID, maxProfileID, minBitsPerMap, maxBitsPerMap, seed int64, randomOrder bool) int {
|
||||
src := rand.NewSource(seed)
|
||||
rng := rand.New(src)
|
||||
|
||||
var bitmapIDs []int
|
||||
if randomOrder {
|
||||
bitmapIDs = rng.Perm(int(maxBitmapID - baseBitmapID))
|
||||
}
|
||||
numrows := 0
|
||||
profileIDs := make(Int64Slice, maxBitsPerMap)
|
||||
for i := baseBitmapID; i < maxBitmapID; i++ {
|
||||
var bitmapID int64
|
||||
if randomOrder {
|
||||
bitmapID = int64(bitmapIDs[i-baseBitmapID])
|
||||
} else {
|
||||
bitmapID = int64(i)
|
||||
}
|
||||
|
||||
numBitsToSet := rng.Int63n(maxBitsPerMap-minBitsPerMap) + minBitsPerMap
|
||||
numrows += int(numBitsToSet)
|
||||
for j := int64(0); j < numBitsToSet; j++ {
|
||||
profileIDs[j] = rng.Int63n(maxProfileID-baseProfileID) + baseProfileID
|
||||
}
|
||||
profIDs := profileIDs[:numBitsToSet]
|
||||
if !randomOrder {
|
||||
sort.Sort(profIDs)
|
||||
}
|
||||
for j := int64(0); j < numBitsToSet; j++ {
|
||||
fmt.Fprintf(w, "%d,%d\n", bitmapID, profIDs[j])
|
||||
}
|
||||
|
||||
}
|
||||
return numrows
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
package bench_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/pilosa/pilosa/bench"
|
||||
)
|
||||
|
||||
func TestImportInit(t *testing.T) {
|
||||
imp := bench.NewImport(os.Stdin, os.Stdout, os.Stderr)
|
||||
imp.BaseBitmapID = 0
|
||||
imp.MaxBitmapID = 10
|
||||
imp.BaseProfileID = 0
|
||||
imp.MaxProfileID = 10
|
||||
imp.RandomBitmapOrder = false
|
||||
imp.MinBitsPerMap = 2
|
||||
imp.MaxBitsPerMap = 3
|
||||
imp.AgentControls = "width"
|
||||
imp.Seed = 0
|
||||
|
||||
imp.Init([]string{"blah"}, 2)
|
||||
f, err := os.Open(imp.Paths[0])
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't open file: %v, err: %v", imp.Paths[0], err)
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading file: %v", err)
|
||||
}
|
||||
|
||||
expected := `
|
||||
0,21
|
||||
0,22
|
||||
1,20
|
||||
1,22
|
||||
2,22
|
||||
2,26
|
||||
3,21
|
||||
3,23
|
||||
4,21
|
||||
4,22
|
||||
5,20
|
||||
5,28
|
||||
6,23
|
||||
6,27
|
||||
7,20
|
||||
7,20
|
||||
8,23
|
||||
8,29
|
||||
9,23
|
||||
9,29
|
||||
`[1:]
|
||||
|
||||
if string(bytes) != expected {
|
||||
t.Fatalf("unexpected result: %v", string(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateImportCSVNonRand(t *testing.T) {
|
||||
b := bytes.NewBuffer(make([]byte, 0))
|
||||
|
||||
bench.GenerateImportCSV(b, 0, 10, 20, 30, 2, 3, 2, false)
|
||||
|
||||
bytes, err := ioutil.ReadAll(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading buffer: %v", err)
|
||||
}
|
||||
|
||||
expected := `
|
||||
0,21
|
||||
0,22
|
||||
1,20
|
||||
1,22
|
||||
2,22
|
||||
2,26
|
||||
3,21
|
||||
3,23
|
||||
4,21
|
||||
4,22
|
||||
5,20
|
||||
5,28
|
||||
6,23
|
||||
6,27
|
||||
7,20
|
||||
7,20
|
||||
8,23
|
||||
8,29
|
||||
9,23
|
||||
9,29
|
||||
`[1:]
|
||||
|
||||
if string(bytes) != expected {
|
||||
t.Fatalf("unexpected value for generated csv: \n%v", string(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateImportCSVRand(t *testing.T) {
|
||||
b := bytes.NewBuffer(make([]byte, 0))
|
||||
|
||||
bench.GenerateImportCSV(b, 0, 10, 21, 29, 1, 4, 0, true)
|
||||
|
||||
bytes, err := ioutil.ReadAll(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading buffer: %v", err)
|
||||
}
|
||||
|
||||
expected := `
|
||||
8,25
|
||||
2,23
|
||||
3,22
|
||||
3,28
|
||||
3,28
|
||||
0,25
|
||||
0,23
|
||||
5,26
|
||||
5,23
|
||||
7,21
|
||||
7,23
|
||||
1,25
|
||||
6,23
|
||||
6,27
|
||||
6,25
|
||||
9,26
|
||||
4,22
|
||||
4,24
|
||||
`[1:]
|
||||
|
||||
if string(bytes) != expected {
|
||||
t.Fatalf("unexpected value for generated csv: \n%v", string(bytes))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
package bench
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MultiDBSetBits sets bits with increasing profile id and bitmap id.
|
||||
type MultiDBSetBits struct {
|
||||
HasClient
|
||||
Name string `json:"name"`
|
||||
BaseBitmapID int `json:"base-bitmap-id"`
|
||||
BaseProfileID int `json:"base-profile-id"`
|
||||
Iterations int `json:"iterations"`
|
||||
Database string `json:"database"`
|
||||
}
|
||||
|
||||
// Init sets up the db name based on the agentNum and sets up the pilosa client.
|
||||
func (b *MultiDBSetBits) Init(hosts []string, agentNum int) error {
|
||||
b.Name = "multi-db-set-bits"
|
||||
b.Database = b.Database + strconv.Itoa(agentNum)
|
||||
return b.HasClient.Init(hosts, agentNum)
|
||||
}
|
||||
|
||||
// Usage returns the usage message to be printed.
|
||||
func (b *MultiDBSetBits) Usage() string {
|
||||
return `
|
||||
multi-db-set-bits sets bits with increasing profile id and bitmap id using a different DB for each agent.
|
||||
|
||||
Agent num changes the database being written to.
|
||||
|
||||
Usage: multi-db-set-bits [arguments]
|
||||
|
||||
The following arguments are available:
|
||||
|
||||
-base-bitmap-id int
|
||||
bits being set will all be greater than base-bitmap-id
|
||||
|
||||
-base-profile-id int
|
||||
profile id num to start from
|
||||
|
||||
-iterations int
|
||||
number of bits to set
|
||||
|
||||
-client-type string
|
||||
Can be 'single' (all agents hitting one host) or 'round_robin'
|
||||
|
||||
-content-type string
|
||||
protobuf or pql
|
||||
|
||||
`[1:]
|
||||
}
|
||||
|
||||
// ConsumeFlags parses all flags up to the next non flag argument (argument does
|
||||
// not start with "-" and isn't the value of a flag). It returns the remaining
|
||||
// args.
|
||||
func (b *MultiDBSetBits) ConsumeFlags(args []string) ([]string, error) {
|
||||
fs := flag.NewFlagSet("MultiDBSetBits", flag.ContinueOnError)
|
||||
fs.SetOutput(ioutil.Discard)
|
||||
fs.IntVar(&b.BaseBitmapID, "base-bitmap-id", 0, "")
|
||||
fs.IntVar(&b.BaseProfileID, "base-profile-id", 0, "")
|
||||
fs.IntVar(&b.Iterations, "iterations", 100, "")
|
||||
fs.StringVar(&b.ClientType, "client-type", "single", "")
|
||||
fs.StringVar(&b.ContentType, "content-type", "protobuf", "")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.Args(), nil
|
||||
}
|
||||
|
||||
// Run runs the MultiDBSetBits benchmark
|
||||
func (b *MultiDBSetBits) Run(ctx context.Context) map[string]interface{} {
|
||||
results := make(map[string]interface{})
|
||||
if b.client == nil {
|
||||
results["error"] = fmt.Errorf("No client set for MultiDBSetBits")
|
||||
return results
|
||||
}
|
||||
s := NewStats()
|
||||
var start time.Time
|
||||
for n := 0; n < b.Iterations; n++ {
|
||||
query := fmt.Sprintf("SetBit(%d, 'frame.n', %d)", b.BaseBitmapID+n, b.BaseProfileID+n)
|
||||
start = time.Now()
|
||||
_, err := b.client.ExecuteQuery(ctx, b.Database, query, true)
|
||||
if err != nil {
|
||||
results["error"] = err
|
||||
return results
|
||||
}
|
||||
s.Add(time.Now().Sub(start))
|
||||
}
|
||||
AddToResults(s, results)
|
||||
return results
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package bench
|
||||
|
||||
// PermutationGenerator provides a way to pass integer IDs through a permutation
|
||||
// map that is pseudorandom but repeatable. This could be done with rand.Perm,
|
||||
// but that would require storing a [Iterations]int64 array, which we want to avoid
|
||||
// for large values of Iterations.
|
||||
// It works by using a Linear Congruence Generator (https://en.wikipedia.org/wiki/Linear_congruential_generator)
|
||||
// with modulus m = Iterations,
|
||||
// c = an arbitrary prime,
|
||||
// a = computed to ensure the full period.
|
||||
// relevant stackoverflow: http://cs.stackexchange.com/questions/29822/lazily-computing-a-random-permutation-of-the-positive-integers
|
||||
type PermutationGenerator struct {
|
||||
a int64
|
||||
c int64
|
||||
m int64
|
||||
}
|
||||
|
||||
func NewPermutationGenerator(m int64, seed int64) *PermutationGenerator {
|
||||
// figure out 'a' and 'c', return PermutationGenerator
|
||||
a := LCGmultiplierFromModulus(m, seed)
|
||||
c := int64(22695479)
|
||||
return &PermutationGenerator{a, c, m}
|
||||
}
|
||||
|
||||
func (p *PermutationGenerator) Next(n int64) int64 {
|
||||
// run one step of the LCG
|
||||
return (n*p.a + p.c) % p.m
|
||||
}
|
||||
|
||||
// LCG parameters must satisfy three conditions:
|
||||
// 1. m and c are relatively prime (satisfied for prime c != m)
|
||||
// 2. a-1 is divisible by all prime factors of m
|
||||
// 3. a-1 is divisible by 4 if m is divisible by 4
|
||||
// Additionally, a seed can be used to select between different permutations
|
||||
func LCGmultiplierFromModulus(m int64, seed int64) int64 {
|
||||
factors := primeFactors(m)
|
||||
product := int64(1)
|
||||
for p := range factors {
|
||||
// satisfy condition 2
|
||||
product *= p
|
||||
}
|
||||
|
||||
if m%4 == 0 {
|
||||
// satisfy condition 3
|
||||
product *= 2
|
||||
}
|
||||
|
||||
return product*seed + 1
|
||||
}
|
||||
|
||||
// Returns map of {integerFactor: count, ...}
|
||||
// This is a naive algorithm that will not work well for large prime n.
|
||||
func primeFactors(n int64) map[int64]int {
|
||||
factors := make(map[int64]int)
|
||||
for i := int64(2); i <= n; i++ {
|
||||
div, mod := n/i, n%i
|
||||
for mod == 0 {
|
||||
factors[i] += 1
|
||||
n = div
|
||||
div, mod = n/i, n%i
|
||||
}
|
||||
}
|
||||
return factors
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package bench
|
||||
|
||||
import "time"
|
||||
|
||||
// wrapper type to force human-readable JSON output
|
||||
type PrettyDuration time.Duration
|
||||
|
||||
// MarshalJSON returns a nicely formatted duration, instead of it just being
|
||||
// treated like an int.
|
||||
func (d PrettyDuration) MarshalJSON() ([]byte, error) {
|
||||
s := time.Duration(d).String()
|
||||
return []byte("\"" + s + "\""), nil
|
||||
}
|
||||
|
||||
// Recursively replaces elements of ugly types with their pretty wrappers
|
||||
func Prettify(m map[string]interface{}) map[string]interface{} {
|
||||
newmap := make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
switch v.(type) {
|
||||
case map[string]interface{}:
|
||||
newmap[k] = Prettify(v.(map[string]interface{}))
|
||||
case []time.Duration:
|
||||
newslice := make([]PrettyDuration, len(v.([]time.Duration)))
|
||||
slice := v.([]time.Duration)
|
||||
for n, e := range slice {
|
||||
newslice[n] = PrettyDuration(e)
|
||||
}
|
||||
newmap[k] = newslice
|
||||
case time.Duration:
|
||||
newmap[k] = PrettyDuration(v.(time.Duration))
|
||||
default:
|
||||
if interv, ok := v.([]map[string]interface{}); ok {
|
||||
for i, iv := range interv {
|
||||
interv[i] = Prettify(iv)
|
||||
}
|
||||
}
|
||||
newmap[k] = v
|
||||
}
|
||||
}
|
||||
return newmap
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
package bench_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pilosa/pilosa/bench"
|
||||
)
|
||||
|
||||
func prettyEncode(data map[string]interface{}) string {
|
||||
pretty := bench.Prettify(data)
|
||||
jsonString := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(jsonString)
|
||||
enc.SetIndent("", " ")
|
||||
err := enc.Encode(pretty)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
|
||||
return jsonString.String()
|
||||
}
|
||||
|
||||
func TestPrettifyString(t *testing.T) {
|
||||
res := make(map[string]interface{}, 1)
|
||||
res["0"] = "foobar"
|
||||
pretty := prettyEncode(res)
|
||||
|
||||
expected := `
|
||||
{
|
||||
"0": "foobar"
|
||||
}
|
||||
`[1:]
|
||||
|
||||
if pretty != expected {
|
||||
t.Fatalf("Pretty string doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrettifyInt(t *testing.T) {
|
||||
res := make(map[string]interface{}, 1)
|
||||
res["0"] = 234567
|
||||
pretty := prettyEncode(res)
|
||||
|
||||
expected := `
|
||||
{
|
||||
"0": 234567
|
||||
}
|
||||
`[1:]
|
||||
|
||||
if pretty != expected {
|
||||
t.Fatalf("Pretty int doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrettifyDuration(t *testing.T) {
|
||||
res := make(map[string]interface{}, 1)
|
||||
res["0"] = time.Duration(234567)
|
||||
pretty := prettyEncode(res)
|
||||
|
||||
expected := `
|
||||
{
|
||||
"0": "234.567µs"
|
||||
}
|
||||
`[1:]
|
||||
|
||||
if pretty != expected {
|
||||
t.Fatalf("Pretty duration doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrettifyDurationSlice(t *testing.T) {
|
||||
res := make(map[string]interface{}, 1)
|
||||
res["0"] = []time.Duration{123, 234567, 34567890}
|
||||
pretty := prettyEncode(res)
|
||||
|
||||
expected := `
|
||||
{
|
||||
"0": [
|
||||
"123ns",
|
||||
"234.567µs",
|
||||
"34.56789ms"
|
||||
]
|
||||
}
|
||||
`[1:]
|
||||
|
||||
if pretty != expected {
|
||||
t.Fatalf("Pretty duration slice doesn't match")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package bench_test
|
||||
|
||||
import (
|
||||
"github.com/pilosa/pilosa/bench"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRandomBitmapCall(t *testing.T) {
|
||||
qm := bench.NewQueryGenerator(5)
|
||||
bmc := qm.RandomBitmapCall(4, 3, 0, 1000)
|
||||
t.Log(bmc.String())
|
||||
}
|
||||
|
||||
func TestRandom(t *testing.T) {
|
||||
for i := 99; i < 120; i++ {
|
||||
qm := bench.NewQueryGenerator(int64(i))
|
||||
call := qm.Random(10, 4, 3, 0, 1000)
|
||||
t.Log(call)
|
||||
}
|
||||
}
|
||||
117
bench/random.go
117
bench/random.go
|
|
@ -1,117 +0,0 @@
|
|||
package bench
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RandomSetBits sets bits randomly and deterministically based on a seed.
|
||||
type RandomSetBits struct {
|
||||
HasClient
|
||||
Name string `json:"name"`
|
||||
BaseBitmapID int64 `json:"base-bitmap-id"`
|
||||
BaseProfileID int64 `json:"base-profile-id"`
|
||||
BitmapIDRange int64 `json:"bitmap-id-range"`
|
||||
ProfileIDRange int64 `json:"profile-id-range"`
|
||||
Iterations int `json:"iterations"`
|
||||
Seed int64 `json:"seed"`
|
||||
DB string `json:"db"`
|
||||
}
|
||||
|
||||
// Init adds the agent num to the random seed and initializes the client.
|
||||
func (b *RandomSetBits) Init(hosts []string, agentNum int) error {
|
||||
b.Name = "random-set-bits"
|
||||
b.Seed = b.Seed + int64(agentNum)
|
||||
return b.HasClient.Init(hosts, agentNum)
|
||||
}
|
||||
|
||||
// Usage returns the usage message to be printed.
|
||||
func (b *RandomSetBits) Usage() string {
|
||||
return `
|
||||
random-set-bits sets random bits
|
||||
|
||||
Agent number modifies the random seed.
|
||||
|
||||
Usage: random-set-bits [arguments]
|
||||
|
||||
The following arguments are available:
|
||||
|
||||
-base-bitmap-id int
|
||||
bits being set will all be greater than BaseBitmapID
|
||||
|
||||
-bitmap-id-range int
|
||||
number of possible bitmap ids that can be set
|
||||
|
||||
-base-profile-id int
|
||||
profile id num to start from
|
||||
|
||||
-profile-id-range int
|
||||
number of possible profile ids that can be set
|
||||
|
||||
-iterations int
|
||||
number of bits to set
|
||||
|
||||
-seed int
|
||||
Seed for RNG
|
||||
|
||||
-db string
|
||||
pilosa db to use
|
||||
|
||||
-client-type string
|
||||
Can be 'single' (all agents hitting one host) or 'round_robin'
|
||||
|
||||
-content-type string
|
||||
protobuf or pql
|
||||
`[1:]
|
||||
}
|
||||
|
||||
// ConsumeFlags parses all flags up to the next non flag argument (argument does
|
||||
// not start with "-" and isn't the value of a flag). It returns the remaining
|
||||
// args.
|
||||
func (b *RandomSetBits) ConsumeFlags(args []string) ([]string, error) {
|
||||
fs := flag.NewFlagSet("RandomSetBits", flag.ContinueOnError)
|
||||
fs.SetOutput(ioutil.Discard)
|
||||
fs.Int64Var(&b.BaseBitmapID, "base-bitmap-id", 0, "")
|
||||
fs.Int64Var(&b.BitmapIDRange, "bitmap-id-range", 100000, "")
|
||||
fs.Int64Var(&b.BaseProfileID, "base-profile-id", 0, "")
|
||||
fs.Int64Var(&b.ProfileIDRange, "profile-id-range", 100000, "")
|
||||
fs.Int64Var(&b.Seed, "seed", 1, "")
|
||||
fs.IntVar(&b.Iterations, "iterations", 100, "")
|
||||
fs.StringVar(&b.DB, "db", "benchdb", "")
|
||||
fs.StringVar(&b.ClientType, "client-type", "single", "")
|
||||
fs.StringVar(&b.ContentType, "content-type", "protobuf", "")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.Args(), nil
|
||||
}
|
||||
|
||||
// Run runs the RandomSetBits benchmark
|
||||
func (b *RandomSetBits) Run(ctx context.Context) map[string]interface{} {
|
||||
src := rand.NewSource(b.Seed)
|
||||
rng := rand.New(src)
|
||||
results := make(map[string]interface{})
|
||||
if b.client == nil {
|
||||
results["error"] = fmt.Errorf("No client set for RandomSetBits")
|
||||
return results
|
||||
}
|
||||
s := NewStats()
|
||||
var start time.Time
|
||||
for n := 0; n < b.Iterations; n++ {
|
||||
bitmapID := rng.Int63n(b.BitmapIDRange)
|
||||
profID := rng.Int63n(b.ProfileIDRange)
|
||||
query := fmt.Sprintf("SetBit(%d, 'frame.n', %d)", b.BaseBitmapID+bitmapID, b.BaseProfileID+profID)
|
||||
start = time.Now()
|
||||
b.client.ExecuteQuery(ctx, b.DB, query, true)
|
||||
s.Add(time.Now().Sub(start))
|
||||
}
|
||||
AddToResults(s, results)
|
||||
return results
|
||||
}
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
package bench
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pilosa/pilosa"
|
||||
"github.com/pilosa/pilosa/pql"
|
||||
)
|
||||
|
||||
// NewSliceHeight creates a new slice height benchmark with stdin/out/err
|
||||
// initialized.
|
||||
func NewSliceHeight(stdin io.Reader, stdout, stderr io.Writer) *SliceHeight {
|
||||
return &SliceHeight{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
}
|
||||
}
|
||||
|
||||
// SliceHeight benchmark tests the effect of an increasing number of bitmaps in
|
||||
// a single slice on query time.
|
||||
type SliceHeight struct {
|
||||
MaxTime time.Duration `json:"max-time"`
|
||||
hosts []string
|
||||
|
||||
Name string `json:"name"`
|
||||
MinBitsPerMap int64 `json:"min-bits-per-map"`
|
||||
MaxBitsPerMap int64 `json:"max-bits-per-map"`
|
||||
Seed int64 `json:"seed"`
|
||||
Database string `json:"database"`
|
||||
Frame string `json:"frame"`
|
||||
|
||||
Stdin io.Reader `json:"-"`
|
||||
Stdout io.Writer `json:"-"`
|
||||
Stderr io.Writer `json:"-"`
|
||||
}
|
||||
|
||||
// Usage returns the usage message to be printed.
|
||||
func (b *SliceHeight) Usage() string {
|
||||
return `
|
||||
slice-height repeatedly imports more bitmaps into a single slice and tests query times in between.
|
||||
|
||||
Agent number has no effect on this benchmark.
|
||||
|
||||
Usage: slice-height [arguments]
|
||||
|
||||
The following arguments are available:
|
||||
|
||||
-max-time int
|
||||
stop benchmark after this many seconds
|
||||
|
||||
-min-bits-per-map int
|
||||
minimum number of bits set per bitmap
|
||||
|
||||
-max-bits-per-map int
|
||||
maximum number of bits set per bitmap
|
||||
|
||||
-seed int
|
||||
seed for RNG
|
||||
|
||||
-db string
|
||||
pilosa db to use
|
||||
|
||||
-frame string
|
||||
frame to import into
|
||||
`[1:]
|
||||
}
|
||||
|
||||
// ConsumeFlags parses all flags up to the next non flag argument (argument does
|
||||
// not start with "-" and isn't the value of a flag). It returns the remaining
|
||||
// args.
|
||||
func (b *SliceHeight) ConsumeFlags(args []string) ([]string, error) {
|
||||
fs := flag.NewFlagSet("SliceHeight", flag.ContinueOnError)
|
||||
fs.SetOutput(ioutil.Discard)
|
||||
|
||||
maxTime := fs.Int("max-time", 30, "")
|
||||
fs.Int64Var(&b.MinBitsPerMap, "min-bits-per-map", 0, "")
|
||||
fs.Int64Var(&b.MaxBitsPerMap, "max-bits-per-map", 10, "")
|
||||
fs.Int64Var(&b.Seed, "seed", 0, "")
|
||||
fs.StringVar(&b.Database, "db", "benchdb", "")
|
||||
fs.StringVar(&b.Frame, "frame", "testframe", "")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.MaxTime = time.Duration(*maxTime) * time.Second
|
||||
return fs.Args(), nil
|
||||
}
|
||||
|
||||
// Init sets up the slice height benchmark.
|
||||
func (b *SliceHeight) Init(hosts []string, agentNum int) error {
|
||||
b.Name = "slice-height"
|
||||
b.hosts = hosts
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run runs the SliceHeight benchmark
|
||||
func (b *SliceHeight) Run(ctx context.Context) map[string]interface{} {
|
||||
results := make(map[string]interface{})
|
||||
|
||||
imp := NewImport(b.Stdin, b.Stdout, b.Stderr)
|
||||
imp.MaxBitmapID = 100
|
||||
imp.MaxProfileID = pilosa.SliceWidth
|
||||
imp.MinBitsPerMap = b.MinBitsPerMap
|
||||
imp.MaxBitsPerMap = b.MaxBitsPerMap
|
||||
imp.Database = b.Database
|
||||
imp.Frame = b.Frame
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for i := 0; i > -1; i++ {
|
||||
iresults := make(map[string]interface{})
|
||||
results["iteration"+strconv.Itoa(i)] = iresults
|
||||
|
||||
genstart := time.Now()
|
||||
imp.Init(b.hosts, 0)
|
||||
gendur := time.Now().Sub(genstart)
|
||||
iresults["csvgen"] = gendur
|
||||
|
||||
iresults["import"] = imp.Run(ctx)
|
||||
|
||||
qstart := time.Now()
|
||||
q := &pql.Call{
|
||||
Name: "TopN",
|
||||
Args: map[string]interface{}{
|
||||
"frame": b.Frame,
|
||||
"n": 50,
|
||||
},
|
||||
}
|
||||
_, err := imp.Client.ExecuteQuery(ctx, b.Database, q.String(), true)
|
||||
if err != nil {
|
||||
iresults["query_error"] = err.Error()
|
||||
} else {
|
||||
qdur := time.Now().Sub(qstart)
|
||||
iresults["query"] = qdur
|
||||
}
|
||||
imp.BaseBitmapID = imp.MaxBitmapID
|
||||
imp.MaxBitmapID = imp.MaxBitmapID * 10
|
||||
|
||||
if time.Now().Sub(start) > b.MaxTime {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
package bench
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Stats object helps track timing stats.
|
||||
type Stats struct {
|
||||
Min time.Duration
|
||||
Max time.Duration
|
||||
Mean time.Duration
|
||||
sumSquareDelta float64
|
||||
Total time.Duration
|
||||
Num int64
|
||||
All []time.Duration
|
||||
SaveAll bool
|
||||
}
|
||||
|
||||
// NewStats gets a Stats object.
|
||||
func NewStats() *Stats {
|
||||
return &Stats{
|
||||
Min: 1<<63 - 1,
|
||||
All: make([]time.Duration, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a new time to the stats object.
|
||||
func (s *Stats) Add(td time.Duration) {
|
||||
if s.SaveAll {
|
||||
s.All = append(s.All, td)
|
||||
}
|
||||
s.Num += 1
|
||||
s.Total += td
|
||||
if td < s.Min {
|
||||
s.Min = td
|
||||
}
|
||||
if td > s.Max {
|
||||
s.Max = td
|
||||
}
|
||||
|
||||
// online variance calculation
|
||||
// https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm
|
||||
delta := td - s.Mean
|
||||
s.Mean += delta / time.Duration(s.Num)
|
||||
s.sumSquareDelta += float64(delta * (td - s.Mean))
|
||||
}
|
||||
|
||||
// Avg returns the average of all durations Added to the Stats object.
|
||||
func (s *Stats) Avg() time.Duration {
|
||||
return s.Total / time.Duration(s.Num)
|
||||
}
|
||||
|
||||
// AddToResults serializes the summary of Stats and adds them to the results
|
||||
// map.
|
||||
func AddToResults(s *Stats, results map[string]interface{}) {
|
||||
results["min"] = s.Min
|
||||
results["max"] = s.Max
|
||||
results["avg"] = s.Mean
|
||||
variance := s.sumSquareDelta / float64(s.Num)
|
||||
results["std"] = time.Duration(math.Sqrt(variance))
|
||||
if s.SaveAll {
|
||||
results["all"] = s.All
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
package bench
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
// NewS3Uploader creates an S3Uploader with specified bucket and key
|
||||
func NewS3Uploader(bucket string, key string) *S3Uploader {
|
||||
return &S3Uploader{
|
||||
bucket,
|
||||
s3.New(session.New(&aws.Config{})),
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
||||
// S3Uploader is an io.Writer for sending output to AWS S3 storage.
|
||||
type S3Uploader struct {
|
||||
bucket string
|
||||
service *s3.S3
|
||||
key string
|
||||
}
|
||||
|
||||
// Write writes data to the uploader's bucket/key
|
||||
func (u *S3Uploader) Write(data []byte) (int, error) {
|
||||
// first return value of PutObject contains an ETag (hash) of the uploaded object, not needed here
|
||||
fmt.Println(string(data))
|
||||
_, err := u.service.PutObject(&s3.PutObjectInput{
|
||||
Body: strings.NewReader(string(data)),
|
||||
Bucket: &u.bucket,
|
||||
Key: &u.key,
|
||||
})
|
||||
|
||||
return 0, err
|
||||
}
|
||||
191
bench/zipf.go
191
bench/zipf.go
|
|
@ -1,191 +0,0 @@
|
|||
package bench
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
|
||||
"context"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Zipf sets random bits according to the Zipf-Mandelbrot distribution.
|
||||
// This distribution accepts two parameters, Exponent and Ratio, for both bitmaps and profiles.
|
||||
// It also uses PermutationGenerator to permute IDs randomly.
|
||||
type Zipf struct {
|
||||
HasClient
|
||||
Name string `json:"name"`
|
||||
BaseBitmapID int64 `json:"base-bitmap-id"`
|
||||
BaseProfileID int64 `json:"base-profile-id"`
|
||||
BitmapIDRange int64 `json:"bitmap-id-range"`
|
||||
ProfileIDRange int64 `json:"profile-id-range"`
|
||||
Iterations int `json:"iterations"`
|
||||
Seed int64 `json:"seed"`
|
||||
DB string `json:"db"`
|
||||
BitmapExponent float64 `json:"bitmap-exponent"`
|
||||
BitmapRatio float64 `json:"bitmap-ratio"`
|
||||
ProfileExponent float64 `json:"profile-exponent"`
|
||||
ProfileRatio float64 `json:"profile-ratio"`
|
||||
Operation string `json:"operation"`
|
||||
bitmapRng *rand.Zipf
|
||||
profileRng *rand.Zipf
|
||||
bitmapPerm *PermutationGenerator
|
||||
profilePerm *PermutationGenerator
|
||||
}
|
||||
|
||||
// Usage returns the usage message to be printed.
|
||||
func (b *Zipf) Usage() string {
|
||||
return `
|
||||
zipf sets random bits according to the Zipf distribution.
|
||||
This is a power-law distribution controlled by two parameters.
|
||||
Exponent, in the range (1, inf), with a default value of 1.001, controls
|
||||
the "sharpness" of the distribution, with higher exponent being sharper.
|
||||
Ratio, in the range (0, 1), with a default value of 0.25, controls the
|
||||
maximum variation of the distribution, with higher ratio being more uniform.
|
||||
|
||||
Agent number modifies random seed.
|
||||
|
||||
Usage: zipf [arguments]
|
||||
|
||||
The following arguments are available:
|
||||
|
||||
-base-bitmap-id int
|
||||
bits being set will all be greater than BaseBitmapID
|
||||
|
||||
-bitmap-id-range int
|
||||
number of possible bitmap ids that can be set
|
||||
|
||||
-base-profile-id int
|
||||
profile id num to start from
|
||||
|
||||
-profile-id-range int
|
||||
number of possible profile ids that can be set
|
||||
|
||||
-iterations int
|
||||
number of bits to set
|
||||
|
||||
-seed int
|
||||
Seed for RNG
|
||||
|
||||
-db string
|
||||
pilosa db to use
|
||||
|
||||
-bitmap-exponent float64
|
||||
zipf exponent parameter for bitmap IDs
|
||||
|
||||
-bitmap-ratio float64
|
||||
zipf probability ratio parameter for bitmap IDs
|
||||
|
||||
-profile-exponent float64
|
||||
zipf exponent parameter for profile IDs
|
||||
|
||||
-profile-ratio float64
|
||||
zipf probability ratio parameter for profile IDs
|
||||
|
||||
-client-type string
|
||||
Can be 'single' (all agents hitting one host) or 'round_robin'
|
||||
|
||||
-operation string
|
||||
Can be 'set' or 'clear'
|
||||
|
||||
-content-type string
|
||||
protobuf or pql
|
||||
`[1:]
|
||||
}
|
||||
|
||||
// ConsumeFlags parses all flags up to the next non flag argument (argument does
|
||||
// not start with "-" and isn't the value of a flag). It returns the remaining
|
||||
// args.
|
||||
func (b *Zipf) ConsumeFlags(args []string) ([]string, error) {
|
||||
fs := flag.NewFlagSet("Zipf", flag.ContinueOnError)
|
||||
fs.SetOutput(ioutil.Discard)
|
||||
fs.Int64Var(&b.BaseBitmapID, "base-bitmap-id", 0, "")
|
||||
fs.Int64Var(&b.BitmapIDRange, "bitmap-id-range", 100000, "")
|
||||
fs.Int64Var(&b.BaseProfileID, "base-profile-id", 0, "")
|
||||
fs.Int64Var(&b.ProfileIDRange, "profile-id-range", 100000, "")
|
||||
fs.Int64Var(&b.Seed, "seed", 1, "")
|
||||
fs.IntVar(&b.Iterations, "iterations", 100, "")
|
||||
fs.StringVar(&b.DB, "db", "benchdb", "")
|
||||
fs.Float64Var(&b.BitmapExponent, "bitmap-exponent", 1.01, "")
|
||||
fs.Float64Var(&b.BitmapRatio, "bitmap-ratio", 0.25, "")
|
||||
fs.Float64Var(&b.ProfileExponent, "profile-exponent", 1.01, "")
|
||||
fs.Float64Var(&b.ProfileRatio, "profile-ratio", 0.25, "")
|
||||
fs.StringVar(&b.ClientType, "client-type", "single", "")
|
||||
fs.StringVar(&b.Operation, "operation", "set", "")
|
||||
fs.StringVar(&b.ContentType, "content-type", "protobuf", "")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.Args(), nil
|
||||
}
|
||||
|
||||
// Offset is the true parameter used by the Zipf distribution, but the ratio,
|
||||
// as defined here, is a simpler, readable way to define the distribution.
|
||||
// Offset is in [1, inf), and its meaning depends on N (a pain for updating benchmark configs)
|
||||
// ratio is in (0, 1), and its meaning does not depend on N.
|
||||
// it is the ratio of the lowest probability in the distribution to the highest.
|
||||
// ratio=0.01 corresponds to a very small offset - the most skewed distribution for a given pair (N, exp)
|
||||
// ratio=0.99 corresponds to a very large offset - the most nearly uniform distribution for a given (N, exp)
|
||||
func getZipfOffset(N int64, exp, ratio float64) float64 {
|
||||
z := math.Pow(ratio, 1/exp)
|
||||
return z * float64(N-1) / (1 - z)
|
||||
}
|
||||
|
||||
// Init sets up the benchmark based on the agent number and initializes the
|
||||
// client.
|
||||
func (b *Zipf) Init(hosts []string, agentNum int) error {
|
||||
b.Name = "zipf"
|
||||
b.Seed = b.Seed + int64(agentNum)
|
||||
rnd := rand.New(rand.NewSource(b.Seed))
|
||||
bitmapOffset := getZipfOffset(b.BitmapIDRange, b.BitmapExponent, b.BitmapRatio)
|
||||
b.bitmapRng = rand.NewZipf(rnd, b.BitmapExponent, bitmapOffset, uint64(b.BitmapIDRange-1))
|
||||
profileOffset := getZipfOffset(b.ProfileIDRange, b.ProfileExponent, b.ProfileRatio)
|
||||
b.profileRng = rand.NewZipf(rnd, b.ProfileExponent, profileOffset, uint64(b.ProfileIDRange-1))
|
||||
|
||||
b.bitmapPerm = NewPermutationGenerator(b.BitmapIDRange, b.Seed)
|
||||
b.profilePerm = NewPermutationGenerator(b.ProfileIDRange, b.Seed+1)
|
||||
|
||||
if b.Operation != "set" && b.Operation != "clear" {
|
||||
return fmt.Errorf("Unsupported operation: \"%s\" (must be \"set\" or \"clear\")", b.Operation)
|
||||
}
|
||||
|
||||
return b.HasClient.Init(hosts, agentNum)
|
||||
}
|
||||
|
||||
// Run runs the Zipf benchmark
|
||||
func (b *Zipf) Run(ctx context.Context) map[string]interface{} {
|
||||
results := make(map[string]interface{})
|
||||
if b.client == nil {
|
||||
results["error"] = fmt.Errorf("No client set for Zipf")
|
||||
return results
|
||||
}
|
||||
operation := "SetBit"
|
||||
if b.Operation == "clear" {
|
||||
operation = "ClearBit"
|
||||
}
|
||||
s := NewStats()
|
||||
var start time.Time
|
||||
for n := 0; n < b.Iterations; n++ {
|
||||
// generate IDs from Zipf distribution
|
||||
bitmapIDOriginal := b.bitmapRng.Uint64()
|
||||
profIDOriginal := b.profileRng.Uint64()
|
||||
// permute IDs randomly, but repeatably
|
||||
bitmapID := b.bitmapPerm.Next(int64(bitmapIDOriginal))
|
||||
profID := b.profilePerm.Next(int64(profIDOriginal))
|
||||
|
||||
query := fmt.Sprintf("%s(%d, 'frame.n', %d)", operation, b.BaseBitmapID+int64(bitmapID), b.BaseProfileID+int64(profID))
|
||||
start = time.Now()
|
||||
_, err := b.client.ExecuteQuery(ctx, b.DB, query, true)
|
||||
if err != nil {
|
||||
results["error"] = fmt.Sprintf("Error executing query in zipf: %v", err)
|
||||
return results
|
||||
}
|
||||
s.Add(time.Now().Sub(start))
|
||||
}
|
||||
AddToResults(s, results)
|
||||
return results
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Binary(pkg, goos, goarch string) (io.Reader, error) {
|
||||
binFile, err := ioutil.TempFile("", "pilosactl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build binary: %v", err)
|
||||
}
|
||||
com := exec.Command("go", "build", "-o", binFile.Name(), pkg)
|
||||
com.Env = append([]string{"GOOS=" + goos, "GOARCH=" + goarch}, os.Environ()...)
|
||||
|
||||
err = com.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Open(binFile.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"benchmarks": [
|
||||
{
|
||||
"num": 1,
|
||||
"name": "import",
|
||||
"args": ["import", "-max-bitmap-id", "100000", "-max-profile-id", "10000", "-max-bits-per-map", "100", "-seed", "0", "-agent-controls", "width", "import", "-max-bitmap-id", "100000", "-max-profile-id", "10000", "-max-bits-per-map", "100", "-seed", "0", "-agent-controls", "width", "-random-bitmap-order", "-db", "randoload"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bufio"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
|
@ -13,27 +12,18 @@ import (
|
|||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pilosa/pilosa"
|
||||
"github.com/pilosa/pilosa/bench"
|
||||
"github.com/pilosa/pilosa/build"
|
||||
"github.com/pilosa/pilosa/creator"
|
||||
"github.com/pilosa/pilosa/pilosactl"
|
||||
"github.com/pilosa/pilosa/roaring"
|
||||
pssh "github.com/pilosa/pilosa/ssh"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -115,9 +105,6 @@ The commands are:
|
|||
inspect inspects fragment data files
|
||||
check performs a consistency check of data files
|
||||
bench benchmarks operations
|
||||
create create pilosa clusters
|
||||
bagent run a benchmarking agent
|
||||
bspawn create a cluster and agents and run benchmarks based on config file
|
||||
|
||||
Use the "-h" flag with any command for more information.
|
||||
`)
|
||||
|
|
@ -157,12 +144,6 @@ func (m *Main) ParseFlags(args []string) error {
|
|||
m.Cmd = NewCheckCommand(m.Stdin, m.Stdout, m.Stderr)
|
||||
case "bench":
|
||||
m.Cmd = NewBenchCommand(m.Stdin, m.Stdout, m.Stderr)
|
||||
case "create":
|
||||
m.Cmd = NewCreateCommand(m.Stdin, m.Stdout, m.Stderr)
|
||||
case "bagent":
|
||||
m.Cmd = NewBagentCommand(m.Stdin, m.Stdout, m.Stderr)
|
||||
case "bspawn":
|
||||
m.Cmd = NewBspawnCommand(m.Stdin, m.Stdout, m.Stderr)
|
||||
default:
|
||||
return ErrUnknownCommand
|
||||
}
|
||||
|
|
@ -1004,672 +985,6 @@ func (cmd *BenchCommand) runSetBit(ctx context.Context, client *pilosa.Client) e
|
|||
return nil
|
||||
}
|
||||
|
||||
// CreateCommand represents a command for creating a pilosa cluster.
|
||||
type CreateCommand struct {
|
||||
ServerN int `json:"serverN"`
|
||||
ReplicaN int `json:"replicaN"`
|
||||
LogFilePrefix string `json:"log-file-prefix"`
|
||||
Hosts []string `json:"hosts"`
|
||||
GoMaxProcs int `json:"gomaxprocs"`
|
||||
|
||||
SSHUser string `json:"ssh-user"`
|
||||
|
||||
CopyBinary bool `json:"copy-binary"`
|
||||
GOOS string `json:"goos"`
|
||||
GOARCH string `json:"goarch"`
|
||||
|
||||
// The following are set by CreateCommand.Run once they are known and exist
|
||||
// to appear in the output
|
||||
LogFiles []string `json:"log-files"`
|
||||
FinalHosts []string `json:"final-hosts"`
|
||||
|
||||
// Standard input/output
|
||||
Stdin io.Reader `json:"-"`
|
||||
Stdout io.Writer `json:"-"`
|
||||
Stderr io.Writer `json:"-"`
|
||||
}
|
||||
|
||||
// NewCreateCommand returns a new instance of CreateCommand.
|
||||
func NewCreateCommand(stdin io.Reader, stdout, stderr io.Writer) *CreateCommand {
|
||||
return &CreateCommand{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseFlags parses command line flags from args.
|
||||
func (cmd *CreateCommand) ParseFlags(args []string) error {
|
||||
fs := flag.NewFlagSet("pilosactl", flag.ContinueOnError)
|
||||
fs.SetOutput(ioutil.Discard)
|
||||
fs.IntVar(&cmd.ServerN, "serverN", 3, "")
|
||||
fs.IntVar(&cmd.ReplicaN, "replicaN", 1, "")
|
||||
fs.StringVar(&cmd.LogFilePrefix, "log-file-prefix", "", "")
|
||||
var hosts string
|
||||
fs.IntVar(&cmd.GoMaxProcs, "gomaxprocs", 0, "")
|
||||
fs.StringVar(&hosts, "hosts", "", "")
|
||||
fs.StringVar(&cmd.SSHUser, "ssh-user", "", "")
|
||||
fs.BoolVar(&cmd.CopyBinary, "copy-binary", false, "")
|
||||
fs.StringVar(&cmd.GOOS, "goos", "linux", "")
|
||||
fs.StringVar(&cmd.GOARCH, "goarch", "amd64", "")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fs.Args()) > 0 {
|
||||
fmt.Fprintf(cmd.Stderr, "Uknown args: %v\n", strings.Join(fs.Args(), " "))
|
||||
return flag.ErrHelp
|
||||
}
|
||||
cmd.Hosts = customSplit(hosts, ",")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Usage returns the usage message to be printed.
|
||||
func (cmd *CreateCommand) Usage() string {
|
||||
return strings.TrimSpace(`
|
||||
usage: pilosactl create [args]
|
||||
|
||||
Creates a pilosa cluster. Defaults to a 3-node in-process cluster.
|
||||
|
||||
The following flags are allowed:
|
||||
|
||||
-serverN
|
||||
number of hosts in cluster. Disregarded if 'hosts' is set
|
||||
|
||||
-replicaN
|
||||
replication factor for cluster
|
||||
|
||||
-hosts
|
||||
comma separated host:port list. If hosts is set, create will start
|
||||
pilosa on these pre-existing hosts. The same host may be listed multiple
|
||||
times with different ports
|
||||
|
||||
-log-file-prefix
|
||||
output from the started cluster will go into files with
|
||||
this prefix (one per node)
|
||||
|
||||
-ssh-user
|
||||
username to use when contacting remote hosts
|
||||
|
||||
-gomaxprocs
|
||||
when starting a cluster on remote hosts, this will set the value
|
||||
of GOMAXPROCS.
|
||||
|
||||
-copy-binary
|
||||
controls whether or not to build and copy pilosa to hosts
|
||||
|
||||
-goos
|
||||
when using copy-binary, GOOS to use while building binary
|
||||
|
||||
-goarch
|
||||
when using copy-binary, GOARCH to use while building binary
|
||||
`)
|
||||
}
|
||||
|
||||
// Run executes cluster creation.
|
||||
func (cmd *CreateCommand) Run(ctx context.Context) error {
|
||||
var clus creator.Cluster
|
||||
if len(cmd.Hosts) == 0 {
|
||||
fmt.Fprintf(cmd.Stderr, "create: no hosts specified - creating cluster in-process\n")
|
||||
clus = &creator.LocalCluster{
|
||||
ReplicaN: cmd.ReplicaN,
|
||||
ServerN: cmd.ServerN,
|
||||
}
|
||||
} else {
|
||||
if cmd.ServerN != 0 {
|
||||
fmt.Fprintf(cmd.Stderr, "create: hosts were specified, so ignoring serverN\n")
|
||||
}
|
||||
clus = &creator.RemoteCluster{
|
||||
ClusterHosts: cmd.Hosts,
|
||||
ReplicaN: cmd.ReplicaN,
|
||||
SSHUser: cmd.SSHUser,
|
||||
Stderr: cmd.Stderr,
|
||||
GoMaxProcs: cmd.GoMaxProcs,
|
||||
CopyBinary: cmd.CopyBinary,
|
||||
GOOS: cmd.GOOS,
|
||||
GOARCH: cmd.GOARCH,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err := clus.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting cluster: %v", err)
|
||||
}
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for range c {
|
||||
fmt.Fprintf(cmd.Stderr, "\ncreate: caught signal - shutting down\n")
|
||||
err := clus.Shutdown()
|
||||
code := 0
|
||||
if err != nil {
|
||||
code = 1
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
}()
|
||||
|
||||
defer clus.Shutdown()
|
||||
cmd.FinalHosts = clus.Hosts()
|
||||
|
||||
logReaders := clus.Logs()
|
||||
if cmd.LogFilePrefix != "" {
|
||||
cmd.LogFiles = make([]string, len(clus.Hosts()))
|
||||
}
|
||||
for i, _ := range clus.Hosts() {
|
||||
var f io.Writer = cmd.Stderr
|
||||
var err error
|
||||
if cmd.LogFilePrefix != "" {
|
||||
f, err = os.Create(cmd.LogFilePrefix + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.LogFiles[i] = f.(*os.File).Name()
|
||||
}
|
||||
|
||||
go func(i int, f io.Writer) {
|
||||
_, err := io.Copy(f, logReaders[i])
|
||||
if err != nil {
|
||||
fmt.Fprintf(cmd.Stderr, "create: error copying cluster logs: '%v'\n", err)
|
||||
}
|
||||
}(i, f)
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(cmd.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
err = enc.Encode(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cmd.Stderr, "create: cluster started.\n")
|
||||
select {}
|
||||
|
||||
}
|
||||
|
||||
// BagentCommand represents a command for running a benchmark agent. A benchmark
|
||||
// agent runs multiple benchmarks in series in the order that they are specified
|
||||
// on the command line.
|
||||
type BagentCommand struct {
|
||||
// Slice of Benchmarks which will be run serially.
|
||||
Benchmarks []bench.Benchmark `json:"benchmarks"`
|
||||
// AgentNum will be passed to each benchmark's Run method so that it can
|
||||
// parameterize its behavior.
|
||||
AgentNum int `json:"agent-num"`
|
||||
|
||||
// Enable pretty printing of results, for human consumption.
|
||||
HumanReadable bool `json:"human-readable"`
|
||||
|
||||
// Slice of pilosa hosts to run the Benchmarks against.
|
||||
Hosts []string `json:"hosts"`
|
||||
|
||||
Stdin io.Reader `json:"-"`
|
||||
Stdout io.Writer `json:"-"`
|
||||
Stderr io.Writer `json:"-"`
|
||||
}
|
||||
|
||||
// NewBagentCommand returns a new instance of BagentCommand.
|
||||
func NewBagentCommand(stdin io.Reader, stdout, stderr io.Writer) *BagentCommand {
|
||||
return &BagentCommand{
|
||||
Benchmarks: []bench.Benchmark{},
|
||||
Hosts: []string{},
|
||||
AgentNum: 0,
|
||||
HumanReadable: false,
|
||||
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseFlags parses command line flags for the BagentCommand. First the command
|
||||
// wide flags `hosts` and `agent-num` are parsed. The rest of the flags should be
|
||||
// a series of subcommands along with their flags. ParseFlags runs each
|
||||
// subcommand's `ConsumeFlags` method which parses the flags for that command
|
||||
// and returns the rest of the argument slice which should contain further
|
||||
// subcommands.
|
||||
func (cmd *BagentCommand) ParseFlags(args []string) error {
|
||||
fs := flag.NewFlagSet("pilosactl", flag.ContinueOnError)
|
||||
fs.SetOutput(ioutil.Discard)
|
||||
|
||||
var pilosaHosts string
|
||||
fs.StringVar(&pilosaHosts, "hosts", "localhost:15000", "")
|
||||
fs.IntVar(&cmd.AgentNum, "agent-num", 0, "")
|
||||
fs.BoolVar(&cmd.HumanReadable, "human", true, "")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
remArgs := fs.Args()
|
||||
if len(remArgs) == 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
for len(remArgs) > 0 {
|
||||
var bm bench.Command
|
||||
var err error
|
||||
switch remArgs[0] {
|
||||
case "-help", "-h":
|
||||
return flag.ErrHelp
|
||||
case "diagonal-set-bits":
|
||||
bm = &bench.DiagonalSetBits{}
|
||||
case "random-set-bits":
|
||||
bm = &bench.RandomSetBits{}
|
||||
case "zipf":
|
||||
bm = &bench.Zipf{}
|
||||
case "multi-db-set-bits":
|
||||
bm = &bench.MultiDBSetBits{}
|
||||
case "random-query":
|
||||
bm = &bench.RandomQuery{}
|
||||
case "import":
|
||||
bm = bench.NewImport(cmd.Stdin, cmd.Stdout, cmd.Stderr)
|
||||
case "slice-height":
|
||||
bm = bench.NewSliceHeight(cmd.Stdin, cmd.Stdout, cmd.Stderr)
|
||||
default:
|
||||
return fmt.Errorf("Unknown benchmark cmd: %v", remArgs[0])
|
||||
}
|
||||
remArgs, err = bm.ConsumeFlags(remArgs[1:])
|
||||
cmd.Benchmarks = append(cmd.Benchmarks, bm)
|
||||
if err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
fmt.Fprintln(cmd.Stderr, bm.Usage())
|
||||
return fmt.Errorf("")
|
||||
}
|
||||
return fmt.Errorf("BagentCommand.ParseFlags: %v", err)
|
||||
}
|
||||
}
|
||||
cmd.Hosts = customSplit(pilosaHosts, ",")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Usage returns the usage message to be printed.
|
||||
func (cmd *BagentCommand) Usage() string {
|
||||
return strings.TrimSpace(`
|
||||
usage: pilosactl bagent [options] <subcommand [options]>...
|
||||
|
||||
Runs benchmarks against a pilosa cluster.
|
||||
|
||||
The following flags are allowed:
|
||||
|
||||
-hosts ("localhost:15000")
|
||||
comma separated list of host:port describing all hosts in the cluster
|
||||
|
||||
-agent-num (0)
|
||||
an integer differentiating this agent from others in the fleet
|
||||
|
||||
-human (true)
|
||||
boolean to enable human-readable format
|
||||
|
||||
subcommands:
|
||||
diagonal-set-bits
|
||||
random-set-bits
|
||||
zipf
|
||||
multi-db-set-bits
|
||||
random-query
|
||||
import
|
||||
slice-height
|
||||
`)
|
||||
}
|
||||
|
||||
// Run executes the benchmark agent.
|
||||
func (cmd *BagentCommand) Run(ctx context.Context) error {
|
||||
sbm := serial(cmd.Benchmarks...)
|
||||
err := sbm.Init(cmd.Hosts, cmd.AgentNum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in cmd.Run initialization: %v", err)
|
||||
}
|
||||
|
||||
res := sbm.Run(ctx)
|
||||
res["agent-num"] = cmd.AgentNum
|
||||
enc := json.NewEncoder(cmd.Stdout)
|
||||
if cmd.HumanReadable {
|
||||
enc.SetIndent("", " ")
|
||||
res = bench.Prettify(res)
|
||||
}
|
||||
err = enc.Encode(res)
|
||||
if err != nil {
|
||||
fmt.Fprintln(cmd.Stderr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BspawnCommand represents a command for spawning complex benchmarks. This
|
||||
// includes cluster creation and teardown, agent creation and teardown, running
|
||||
// multiple benchmarks in series and/or parallel, and collecting all the
|
||||
// results.
|
||||
type BspawnCommand struct {
|
||||
// If PilosaHosts is specified, CreatorArgs is ignored and the existing
|
||||
// cluster specified here is used.
|
||||
PilosaHosts []string `json:"pilosa-hosts"`
|
||||
|
||||
// CreateCommand will be used with these arguments to create a cluster -
|
||||
// the cluster will be used to populate the PilosaHosts field. This
|
||||
// should include everything that comes after `pilosactl create`
|
||||
CreatorArgs []string `json:"creator-args"`
|
||||
|
||||
// List of hosts to run agents on. If this is empty, agents will be run
|
||||
// locally.
|
||||
AgentHosts []string `json:"agent-hosts"`
|
||||
|
||||
// Makes output human readable
|
||||
HumanReadable bool `json:"human-readable"`
|
||||
|
||||
// Result destination, ["stdout", "s3"]
|
||||
Output string `json:"output"`
|
||||
|
||||
// If this is true, build and copy pilosactl binary to agent hosts.
|
||||
CopyBinary bool `json:"copy-binary"`
|
||||
GOOS string `json:"goos"`
|
||||
GOARCH string `json:"goarch"`
|
||||
|
||||
// Benchmarks is a slice of Spawns which specifies all of the bagent
|
||||
// commands to run. These will all be run in parallel, started on each
|
||||
// of the agents in a round robin fashion.
|
||||
Benchmarks []Spawn `json:"benchmarks"`
|
||||
|
||||
SSHUser string `json:"ssh-user"`
|
||||
|
||||
Stdin io.Reader `json:"-"`
|
||||
Stdout io.Writer `json:"-"`
|
||||
Stderr io.Writer `json:"-"`
|
||||
}
|
||||
|
||||
// Spawn represents a bagent command run in parallel across Num agents. The
|
||||
// bagent command can run multiple Benchmarks serially within itself.
|
||||
type Spawn struct {
|
||||
Num int `json:"num"` // number of agents to run
|
||||
Name string `json:"name"` // Should describe what this Spawn does
|
||||
Args []string `json:"args"` // everything that comes after `pilosactl bagent [arguments]`
|
||||
}
|
||||
|
||||
// NewBspawnCommand returns a new instance of BspawnCommand.
|
||||
func NewBspawnCommand(stdin io.Reader, stdout, stderr io.Writer) *BspawnCommand {
|
||||
return &BspawnCommand{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseFlags parses command line flags from args.
|
||||
func (cmd *BspawnCommand) ParseFlags(args []string) error {
|
||||
fs := flag.NewFlagSet("pilosactl", flag.ContinueOnError)
|
||||
fs.SetOutput(ioutil.Discard)
|
||||
creatorHosts := fs.String("creator.hosts", "", "")
|
||||
creatorLFP := fs.String("creator.log-file-prefix", "", "")
|
||||
creatorCopyBinary := fs.Bool("creator.copy-binary", false, "")
|
||||
pilosaHosts := fs.String("pilosa-hosts", "", "")
|
||||
agentHosts := fs.String("agent-hosts", "", "")
|
||||
sshUser := fs.String("ssh-user", "", "")
|
||||
fs.BoolVar(&cmd.HumanReadable, "human", true, "")
|
||||
fs.StringVar(&cmd.Output, "output", "stdout", "")
|
||||
fs.BoolVar(&cmd.CopyBinary, "copy-binary", false, "")
|
||||
fs.StringVar(&cmd.GOOS, "goos", "linux", "")
|
||||
fs.StringVar(&cmd.GOARCH, "goarch", "amd64", "")
|
||||
|
||||
err := fs.Parse(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fs.Args()) != 1 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
f, err := os.Open(fs.Args()[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dec := json.NewDecoder(f)
|
||||
err = dec.Decode(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *pilosaHosts != "" {
|
||||
cmd.PilosaHosts = customSplit(*pilosaHosts, ",")
|
||||
}
|
||||
if *agentHosts != "" {
|
||||
cmd.AgentHosts = customSplit(*agentHosts, ",")
|
||||
}
|
||||
if *sshUser != "" {
|
||||
cmd.SSHUser = *sshUser
|
||||
}
|
||||
if *pilosaHosts == "" {
|
||||
cmd.CreatorArgs = []string{"-hosts=" + *creatorHosts, "-log-file-prefix=" + *creatorLFP, "-ssh-user=" + cmd.SSHUser, "-goos=" + cmd.GOOS, "-goarch=" + cmd.GOARCH}
|
||||
if *creatorCopyBinary {
|
||||
cmd.CreatorArgs = append(cmd.CreatorArgs, "-copy-binary")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Usage returns the usage message to be printed.
|
||||
func (cmd *BspawnCommand) Usage() string {
|
||||
return strings.TrimSpace(`
|
||||
usage: pilosactl spawn [flags] <configfile>
|
||||
|
||||
Benchmark orchestration tool - runs 'create' and potentially multiple instances
|
||||
of 'bagent' spread across a number of hosts.
|
||||
|
||||
The following flags are allowed and will override the values in the config file:
|
||||
|
||||
-creator.hosts ()
|
||||
hosts argument for pilosactl create
|
||||
|
||||
-creator.log-file-prefix ()
|
||||
log-file-prefix argument for pilosactl create. If empty, log to stderr.
|
||||
|
||||
-creator.copy-binary (false)
|
||||
pilosactl create should build and copy pilosa binary to cluster
|
||||
|
||||
-pilosa-hosts ([])
|
||||
pilosa hosts to run against (will ignore creator args)
|
||||
|
||||
-agent-hosts ("localhost")
|
||||
hosts to use for benchmark agents
|
||||
|
||||
-ssh-user (current username)
|
||||
username to use when contacting remote hosts
|
||||
|
||||
-human (true)
|
||||
toggle human readable output (indented json)
|
||||
|
||||
-output ("stdout")
|
||||
string to select output destination, "stdout" or "s3"
|
||||
|
||||
-copy-binary (false)
|
||||
controls whether or not to build and copy pilosactl to agents
|
||||
|
||||
-goos (linux)
|
||||
when using copy-binary, GOOS to use while building binary
|
||||
|
||||
-goarch (amd64)
|
||||
when using copy-binary, GOARCH to use while building binary
|
||||
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
// Run executes the main program execution.
|
||||
func (cmd *BspawnCommand) Run(ctx context.Context) error {
|
||||
runUUID := uuid.NewV1()
|
||||
output := make(map[string]interface{})
|
||||
output["run-uuid"] = runUUID.String()
|
||||
if len(cmd.PilosaHosts) == 0 {
|
||||
fmt.Fprintln(cmd.Stderr, "bspawn: pilosa-hosts not specified - using create command to build cluster")
|
||||
r, w := io.Pipe()
|
||||
createCmd := NewCreateCommand(cmd.Stdin, w, cmd.Stderr)
|
||||
err := createCmd.ParseFlags(cmd.CreatorArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
err := createCmd.Run(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(cmd.Stderr, "bspawn: cluster creation error while spawning: %v\n", err)
|
||||
}
|
||||
}()
|
||||
clus := &CreateCommand{}
|
||||
dec := json.NewDecoder(r)
|
||||
err = dec.Decode(clus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.PilosaHosts = clus.FinalHosts
|
||||
output["cluster"] = clus
|
||||
}
|
||||
if len(cmd.AgentHosts) == 0 {
|
||||
fmt.Fprintln(cmd.Stderr, "bpspawn: no agent-hosts specified; all agents will be spawned on localhost")
|
||||
cmd.AgentHosts = []string{"localhost"}
|
||||
}
|
||||
output["spawn"] = cmd
|
||||
res, err := cmd.spawnRemote(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output["results"] = res
|
||||
|
||||
var writer io.Writer
|
||||
if cmd.Output == "s3" {
|
||||
writer = bench.NewS3Uploader("benchmarks-pilosa", runUUID.String()+".json")
|
||||
} else if cmd.Output == "stdout" {
|
||||
writer = cmd.Stdout
|
||||
} else {
|
||||
return fmt.Errorf("invalid bspawn output destination")
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(writer)
|
||||
if cmd.HumanReadable {
|
||||
enc.SetIndent("", " ")
|
||||
output = bench.Prettify(output)
|
||||
}
|
||||
return enc.Encode(output)
|
||||
}
|
||||
|
||||
func (cmd *BspawnCommand) spawnRemote(ctx context.Context) (map[string]interface{}, error) {
|
||||
agentIdx := 0
|
||||
agentFleet, err := pssh.NewFleet(cmd.AgentHosts, cmd.SSHUser, "", cmd.Stderr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmdName := "pilosactl"
|
||||
if cmd.CopyBinary {
|
||||
cmdName = "/tmp/pilosactl" + strconv.Itoa(rand.Int())
|
||||
fmt.Fprintf(cmd.Stderr, "bspawn: building pilosactl binary with GOOS=%v and GOARCH=%v to copy to agents at %v\n", cmd.GOOS, cmd.GOARCH, cmdName)
|
||||
pkg := "github.com/pilosa/pilosa/cmd/pilosactl"
|
||||
bin, err := build.Binary(pkg, cmd.GOOS, cmd.GOARCH)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = agentFleet.WriteFile(cmdName, "+x", bin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sessions := make([]*ssh.Session, 0)
|
||||
results := make(map[string]interface{})
|
||||
resLock := sync.Mutex{}
|
||||
wg := sync.WaitGroup{}
|
||||
fmt.Fprintln(cmd.Stderr, "bspawn: running benchmarks")
|
||||
for _, sp := range cmd.Benchmarks {
|
||||
results[sp.Name] = make(map[int]interface{})
|
||||
for i := 0; i < sp.Num; i++ {
|
||||
agentIdx %= len(cmd.AgentHosts)
|
||||
sess, err := agentFleet[cmd.AgentHosts[agentIdx]].NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agentIdx += 1
|
||||
sessions = append(sessions, sess)
|
||||
stdout, err := sess.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(stdout io.Reader, name string, num int) {
|
||||
defer wg.Done()
|
||||
dec := json.NewDecoder(stdout)
|
||||
var v interface{}
|
||||
err := dec.Decode(&v)
|
||||
if err != nil {
|
||||
fmt.Fprintf(cmd.Stderr, "error decoding json: %v, spawn: %v\n", err, name)
|
||||
}
|
||||
resLock.Lock()
|
||||
results[name].(map[int]interface{})[num] = v
|
||||
resLock.Unlock()
|
||||
}(stdout, sp.Name, i)
|
||||
sess.Stderr = cmd.Stderr
|
||||
err = sess.Start(cmdName + " bagent -agent-num=" + strconv.Itoa(i) + " -hosts=" + strings.Join(cmd.PilosaHosts, ",") + " " + strings.Join(sp.Args, " "))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, sess := range sessions {
|
||||
err = sess.Wait()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error waiting for remote bagent: %v", err)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
return results, nil
|
||||
}
|
||||
|
||||
type serialBenchmark struct {
|
||||
benchmarkers []bench.Benchmark
|
||||
}
|
||||
|
||||
// Init calls Init for each benchmark. If there are any errors, it will return a
|
||||
// non-nil error value.
|
||||
func (sb *serialBenchmark) Init(hosts []string, agentNum int) error {
|
||||
errors := make([]error, len(sb.benchmarkers))
|
||||
hadErr := false
|
||||
for i, b := range sb.benchmarkers {
|
||||
errors[i] = b.Init(hosts, agentNum)
|
||||
if errors[i] != nil {
|
||||
hadErr = true
|
||||
}
|
||||
}
|
||||
if hadErr {
|
||||
return fmt.Errorf("Had errs in serialBenchmark.Init: %v", errors)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run runs the serial benchmark and returns it's results in a nested map - the
|
||||
// top level keys are the indices of each benchmark in the list of benchmarks,
|
||||
// and the values are the results of each benchmark's Run method.
|
||||
func (sb *serialBenchmark) Run(ctx context.Context) map[string]interface{} {
|
||||
benchmarks := make([]map[string]interface{}, len(sb.benchmarkers))
|
||||
results := map[string]interface{}{"benchmarks": benchmarks}
|
||||
|
||||
total_start := time.Now()
|
||||
for i, b := range sb.benchmarkers {
|
||||
start := time.Now()
|
||||
output := b.Run(ctx)
|
||||
if _, ok := output["runtime"]; ok {
|
||||
panic(fmt.Sprintf("Benchmark %v added 'runtime' to its results", b))
|
||||
}
|
||||
output["runtime"] = time.Now().Sub(start)
|
||||
ret := map[string]interface{}{"output": output, "metadata": b}
|
||||
benchmarks[i] = ret
|
||||
}
|
||||
results["total_runtime"] = time.Now().Sub(total_start)
|
||||
return results
|
||||
}
|
||||
|
||||
// serial takes a variable number of Benchmarks and returns a Benchmark
|
||||
// which combines then and will run each serially.
|
||||
func serial(bs ...bench.Benchmark) bench.Benchmark {
|
||||
return &serialBenchmark{
|
||||
benchmarkers: bs,
|
||||
}
|
||||
}
|
||||
|
||||
// readCSVRow reads a bitmap/profile pair from a CSV row.
|
||||
func readCSVRow(r *csv.Reader) (bitmapID, profileID uint64, timestamp int64, err error) {
|
||||
// Read CSV row.
|
||||
|
|
@ -1709,12 +1024,5 @@ func readCSVRow(r *csv.Reader) (bitmapID, profileID uint64, timestamp int64, err
|
|||
return bitmapID, profileID, timestamp, nil
|
||||
}
|
||||
|
||||
func customSplit(s string, sep string) []string {
|
||||
if s == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(s, sep)
|
||||
}
|
||||
|
||||
// errBlank indicates a blank row in a CSV file.
|
||||
var errBlank = errors.New("blank row")
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"benchmarks": [
|
||||
{
|
||||
"num": 3,
|
||||
"args": ["multi-db-set-bits", "-iterations", "30000", "-client-type", "round_robin"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"benchmarks": [
|
||||
{
|
||||
"num": 3,
|
||||
"args": [
|
||||
"random-set-bits", "-iterations", "30000", "-profile-id-range", "1000000", "-bitmap-id-range", "1000000", "-seed", "2345", "-client-type", "round_robin", "-db", "randsetandquery",
|
||||
"random-query", "-iterations", "1000", "-bitmap-id-range", "1000000", "-seed", "1239", "-dbs", "randsetandquery"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"benchmarks": [
|
||||
{
|
||||
"num": 3,
|
||||
"args": ["random-set-bits", "-iterations", "30000", "-profile-id-range", "1000000", "-bitmap-id-range", "1000000", "-seed", "2345", "-client-type", "round_robin"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"benchmarks": [
|
||||
{
|
||||
"num": 1,
|
||||
"args": ["-human", "slice-height", "-max-time", "1", "-max-bits-per-map", "100"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"benchmarks": [
|
||||
{
|
||||
"num": 3,
|
||||
"name": "set-diags",
|
||||
"args": ["diagonal-set-bits", "-iterations", "30000", "-client-type", "round_robin"]
|
||||
},
|
||||
{
|
||||
"num": 2,
|
||||
"name": "rand-plus-zipf",
|
||||
"args": ["random-set-bits", "-iterations", "20000", "zipf", "-iterations", "100"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"benchmarks": [
|
||||
{
|
||||
"num": 1,
|
||||
"args": ["zipf", "-iterations", "10000", "-profile-id-range", "100", "-bitmap-id-range", "100", "-seed", "2345", "-client-type", "round_robin", "-bitmap-exponent", "1.001", "-bitmap-ratio", ".9", "-profile-exponent", "1.001", "-profile-ratio", ".3"]
|
||||
},
|
||||
{
|
||||
"num": 1,
|
||||
"args": ["zipf", "-iterations", "10000", "-profile-id-range", "100", "-bitmap-id-range", "100", "-seed", "2345", "-client-type", "round_robin", "-bitmap-exponent", "1.001", "-bitmap-ratio", ".9", "-profile-exponent", "1.001", "-profile-ratio", ".3", "-operation", "clear"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// creator contains code for standing up pilosa clusters
|
||||
package creator
|
||||
|
||||
import "io"
|
||||
|
||||
type Cluster interface {
|
||||
Start() error
|
||||
Hosts() []string
|
||||
Shutdown() error
|
||||
Logs() []io.Reader
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
package creator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/pilosa/pilosa"
|
||||
)
|
||||
|
||||
type LocalCluster struct {
|
||||
ReplicaN int
|
||||
ServerN int
|
||||
hosts []string
|
||||
logs []io.Reader
|
||||
servers []*pilosa.Server
|
||||
cluster *pilosa.Cluster
|
||||
path string
|
||||
}
|
||||
|
||||
func (localCluster *LocalCluster) Start() error {
|
||||
BasePort := 19327
|
||||
|
||||
localCluster.hosts = make([]string, localCluster.ServerN)
|
||||
localCluster.servers = make([]*pilosa.Server, localCluster.ServerN)
|
||||
localCluster.logs = make([]io.Reader, localCluster.ServerN)
|
||||
|
||||
path, err := ioutil.TempDir("", "pilosa-bench-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localCluster.path = path
|
||||
|
||||
// Build cluster configuration.
|
||||
cluster := pilosa.NewCluster()
|
||||
cluster.ReplicaN = localCluster.ReplicaN
|
||||
|
||||
for i := 0; i < localCluster.ServerN; i++ {
|
||||
cluster.Nodes = append(cluster.Nodes, &pilosa.Node{
|
||||
Host: fmt.Sprintf("localhost:%d", BasePort+i),
|
||||
})
|
||||
}
|
||||
localCluster.cluster = cluster
|
||||
|
||||
// Build servers.
|
||||
for i := range localCluster.servers {
|
||||
// Make server work directory.
|
||||
if err := os.MkdirAll(filepath.Join(path, strconv.Itoa(i)), 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build server.
|
||||
s := pilosa.NewServer()
|
||||
s.Host = fmt.Sprintf("localhost:%d", BasePort+i)
|
||||
s.Cluster = cluster
|
||||
s.Index.Path = filepath.Join(path, strconv.Itoa(i), "data")
|
||||
|
||||
// Create log stream
|
||||
localCluster.logs[i], s.LogOutput = io.Pipe()
|
||||
|
||||
localCluster.servers[i] = s
|
||||
}
|
||||
|
||||
// Open all servers.
|
||||
for i, s := range localCluster.servers {
|
||||
if err := s.Open(); err != nil {
|
||||
return err
|
||||
}
|
||||
localCluster.hosts[i] = s.Host
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LocalCluster) Hosts() []string { return c.hosts }
|
||||
func (c *LocalCluster) Logs() []io.Reader { return c.logs }
|
||||
func (c *LocalCluster) Shutdown() error {
|
||||
errs := ""
|
||||
for _, s := range c.servers {
|
||||
if err := s.Close(); err != nil {
|
||||
errs = errs + err.Error() + "; "
|
||||
}
|
||||
}
|
||||
if err := os.RemoveAll(c.path); err != nil {
|
||||
errs = errs + err.Error() + ";"
|
||||
}
|
||||
if errs != "" {
|
||||
return fmt.Errorf(errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
package creator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/pilosa/pilosa"
|
||||
"github.com/pilosa/pilosa/build"
|
||||
pssh "github.com/pilosa/pilosa/ssh"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type RemoteCluster struct {
|
||||
ClusterHosts []string
|
||||
ReplicaN int
|
||||
SSHUser string
|
||||
Keyfile string
|
||||
Key []byte
|
||||
GoMaxProcs int
|
||||
CopyBinary bool
|
||||
GOOS string
|
||||
GOARCH string
|
||||
Stderr io.Writer
|
||||
wg *sync.WaitGroup
|
||||
logs []io.Reader
|
||||
sessions []*ssh.Session
|
||||
pipeRs []*io.PipeReader
|
||||
pipeWs []*io.PipeWriter
|
||||
stdins []io.WriteCloser
|
||||
}
|
||||
|
||||
// Start creates a configuration for each host in the cluster, copies it to the
|
||||
// node, and starts the pilosa process on the remote host.
|
||||
func (c *RemoteCluster) Start() error {
|
||||
c.logs = make([]io.Reader, 0)
|
||||
if len(c.ClusterHosts) == 0 {
|
||||
return fmt.Errorf("no type or hosts specified - cannot continue")
|
||||
}
|
||||
|
||||
fleet, err := pssh.NewFleet(c.ClusterHosts, c.SSHUser, c.Keyfile, c.Stderr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to cluster hosts: %v", err)
|
||||
}
|
||||
cmdName := "pilosa"
|
||||
if c.CopyBinary {
|
||||
cmdName = "/tmp/pilosa" + strconv.Itoa(rand.Int())
|
||||
fmt.Fprintf(c.Stderr, "create: building pilosa binary with GOOS=%v and GOARCH=%v to copy to hosts at %v", c.GOOS, c.GOARCH, cmdName)
|
||||
|
||||
pkg := "github.com/pilosa/pilosa/cmd/pilosa"
|
||||
bin, err := build.Binary(pkg, c.GOOS, c.GOARCH)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building binary: %v", err)
|
||||
}
|
||||
|
||||
err = fleet.WriteFile(cmdName, "+x", bin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing binary to fleet: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// build config
|
||||
conf := pilosa.NewConfigForHosts(c.ClusterHosts)
|
||||
conf.Cluster.ReplicaN = c.ReplicaN
|
||||
|
||||
// copy config to remote hosts and start pilosa
|
||||
c.wg = &sync.WaitGroup{}
|
||||
for _, hostport := range c.ClusterHosts {
|
||||
|
||||
// Set up config for this host
|
||||
host, port, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
return fmt.Errorf("splitting hostport: %v", err)
|
||||
}
|
||||
conf.Host = hostport
|
||||
conf.DataDir = "~/.pilosa" + port
|
||||
|
||||
// Get client for host
|
||||
client, err := fleet.Get(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to host: %v", err)
|
||||
}
|
||||
configname := "pilosa" + port + ".conf"
|
||||
w, err := client.OpenFile(configname, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening remote config file: %v", err)
|
||||
}
|
||||
enc := toml.NewEncoder(w)
|
||||
err = enc.Encode(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding config: %v", err)
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing config writer: %v", err)
|
||||
}
|
||||
|
||||
// Start pilosa on remote host
|
||||
sess, err := client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Have to request pty in order to be able to kill remote process
|
||||
// reliably.
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ISIG: 1,
|
||||
ssh.ECHO: 0,
|
||||
}
|
||||
err = sess.RequestPty("vt100", 40, 80, modes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request pty error: %v", err)
|
||||
}
|
||||
pipeR, pipeW := io.Pipe()
|
||||
sess.Stdout = pipeW
|
||||
sess.Stderr = pipeW
|
||||
inpipe, err := sess.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.logs = append(c.logs, pipeR)
|
||||
c.sessions = append(c.sessions, sess)
|
||||
c.pipeRs = append(c.pipeRs, pipeR)
|
||||
c.pipeWs = append(c.pipeWs, pipeW)
|
||||
c.stdins = append(c.stdins, inpipe)
|
||||
|
||||
gomaxprocsString := ""
|
||||
if c.GoMaxProcs != 0 {
|
||||
gomaxprocsString = "GOMAXPROCS=" + strconv.Itoa(c.GoMaxProcs) + " "
|
||||
}
|
||||
|
||||
err = sess.Start(gomaxprocsString + cmdName + " -config " + configname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
err = sess.Wait()
|
||||
if err != nil {
|
||||
fmt.Fprintf(c.Stderr, "problem with remote pilosa process: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RemoteCluster) Hosts() []string { return c.ClusterHosts }
|
||||
func (c *RemoteCluster) Logs() []io.Reader { return c.logs }
|
||||
func (c *RemoteCluster) Shutdown() error {
|
||||
for i, sess := range c.sessions {
|
||||
var err error
|
||||
_, err = c.stdins[i].Write([]byte{3}) // Send Control C
|
||||
if err != nil {
|
||||
fmt.Fprintf(c.Stderr, "Error write-signaling remote process: %v\n", err)
|
||||
}
|
||||
// signaling isn't supported by many ssh servers - hence the hack above
|
||||
err = sess.Signal(ssh.SIGINT)
|
||||
if err != nil {
|
||||
fmt.Fprintf(c.Stderr, "Error signaling remote process: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
c.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-time.After(time.Second * 5):
|
||||
for _, sess := range c.sessions {
|
||||
err := sess.Close()
|
||||
if err != nil {
|
||||
fmt.Fprintf(c.Stderr, "Error closing remote session: %v\n", err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("timed out waiting for remote processes to exit")
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue