Repository: bojand/ghz (go)

Issue:
Significant performance drop between v0.103.0 and v0.104.0
Hello!
First of all, thanks for for this amazing tool. We are using it in https://github.com/LesnyRumcajs/grpc_bench to benchmark gRPC implementations across different programming languages and technologies.

I'm using `ghz` as a docker image and noticed a **significant** drop in performance between tag v0.103.0 and v0.104.0. Had to do some binary search to pinpoint the exact versions. :)

### tl;dr
Before v0.104.0, a single `ghz` container constrained to 1 CPU could generate (with a given payload / benchmark scenario) 6628 req/s to a Rust gRPC service (which would make Rust implementation use ~11% CPU). Right now `ghz` is able to generate ~5-6x times less load.

### long story

Using locally https://hub.docker.com/r/obvionaoe/ghz (it also applies to the one that was introduced to this repo recently).

v.0.103.0
```
-----------------------------------------------------------------------------------------------------------------------------------------
| name                        |   req/s |   avg. latency |        90 % in |        95 % in |        99 % in | avg. cpu |   avg. memory |
-----------------------------------------------------------------------------------------------------------------------------------------
| rust_tonic_st               |    6628 |      107.35 ms |      198.05 ms |      203.40 ms |      396.92 ms |   11.25% |     10.95 MiB |
-----------------------------------------------------------------------------------------------------------------------------------------
Benchmark Execution Parameters:
4c97b2f Fri, 18 Feb 2022 15:15:40 +0100 GitHub update scala_fs2 version (#195)
- GRPC_BENCHMARK_DURATION=20s
- GRPC_BENCHMARK_WARMUP=5s
- GRPC_SERVER_CPUS=1
- GRPC_SERVER_RAM=512m
- GRPC_CLIENT_CONNECTIONS=50
- GRPC_CLIENT_CONCURRENCY=1000
- GRPC_CLIENT_QPS=0
- GRPC_CLIENT_CPUS=1
- GRPC_REQUEST_SCENARIO=complex_proto

```

v0.104.0
```
-----------------------------------------------------------------------------------------------------------------------------------------
| name                        |   req/s |   avg. latency |        90 % in |        95 % in |        99 % in | avg. cpu |   avg. memory |
-----------------------------------------------------------------------------------------------------------------------------------------
| rust_tonic_st               |    1178 |      290.81 ms |      599.03 ms |         1.90 s |         4.30 s |     4.6% |      8.86 MiB |
-----------------------------------------------------------------------------------------------------------------------------------------
Benchmark Execution Parameters:
4c97b2f Fri, 18 Feb 2022 15:15:40 +0100 GitHub update scala_fs2 version (#195)
- GRPC_BENCHMARK_DURATION=20s
- GRPC_BENCHMARK_WARMUP=5s
- GRPC_SERVER_CPUS=1
- GRPC_SERVER_RAM=512m
- GRPC_CLIENT_CONNECTIONS=50
- GRPC_CLIENT_CONCURRENCY=1000
- GRPC_CLIENT_QPS=0
- GRPC_CLIENT_CPUS=1
- GRPC_REQUEST_SCENARIO=complex_proto
```

You can look at global results (I believe using the latest docker image from this repo) here: https://github.com/LesnyRumcajs/grpc_bench/wiki/2022-02-18-bench-results

Same benchmark but using some old ghz: https://github.com/LesnyRumcajs/grpc_bench/wiki/2022-02-19-bench-results

In our benchmark case it is a major issue because we can't, even on a decent machine, saturate a single CPU Rust implementation, not to mention multi-CPU scenarios.

It would be nice to find the root cause and perhaps setup some tests (even manual ones, before release) to check if the performance is more or less the same. Unfortunately I'm not versed in Golang so I can't contribute more than that. :(

Pull request:
add disable-template-funcs and disable-template-data option
Adds two options in hope to resolve #340.

**`--disable-template-functions`**

Disable execution of template functions within call data and metadata. This can be useful for some performance improvements. Note that if template functions are used within data with this option set to `true`, it will result in an error. If `--disable-template-data` is set to `true` this is automatically also set to `true`.

**`--disable-template-data`**

Disable execution of templates within call data and metadata. This can be useful for some performance improvements. This automatically also sets `disable-template-functions` to `true`.

Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index a233ee0..70a4e2d 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -31,13 +31,13 @@ jobs:
     runs-on: ubuntu-latest
     env:
       GO111MODULE: on
-      GOLANGCI_LINT_VERSION: v1.46.2
+      GOLANGCI_LINT_VERSION: v1.49.0
 
     steps:
     - name: Set up Go
       uses: actions/setup-go@v3
       with:
-        go-version: 1.18
+        go-version: 1.19
       id: go
     - name: Check out code
       uses: actions/checkout@v3
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index fe565b4..2677a5f 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -32,7 +32,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v3
         with:
-          go-version: 1.18
+          go-version: 1.19
 
       - name: Get current time
         uses: gerred/actions/current-time@master
diff --git a/.golangci.yml b/.golangci.yml
index 0d22677..f5279a1 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -11,11 +11,11 @@ issues:
     # Since we purposefully do that. See comments in code.
     - path: runner/requester.go
       text: "lostcancel"
-    
+
     # TODO Look into fixing time.Tick() usage SA1015 in worker.go
     - path: runner/worker.go
       text: "SA1015"
-    
+
     # We intentionally assign nil to err
     - path: runner/worker.go
       text: "ineffectual assignment to `err`"
@@ -30,13 +30,12 @@ issues:
 
     # TODO fix protobuf deprecated
     - path: runner/
-      text: "SA1019: package github.com/golang/protobuf"
-  
+      text: 'SA1019: "github.com/golang/protobuf/proto"'
+
     # TODO fix protobuf deprecated
     - path: protodesc/
-      text: "SA1019: package github.com/golang/protobuf"
+      text: 'SA1019: "github.com/golang/protobuf/proto"'
 
     # TODO fix protobuf deprecated
     - path: runner/
-      text: "SA1019: \"github.com/golang/protobuf/jsonpb\" is deprecated"
-
+      text: 'SA1019: "github.com/golang/protobuf/jsonpb" is deprecated'
diff --git a/cmd/ghz/main.go b/cmd/ghz/main.go
index e1c48fd..f6fa94b 100644
--- a/cmd/ghz/main.go
+++ b/cmd/ghz/main.go
@@ -266,6 +266,14 @@ var (
 	maxSendMsgSize      = kingpin.Flag("max-send-message-size", "Maximum message size the client can send.").
 				PlaceHolder(" ").IsSetByUser(&isMaxSendMsgSizeSet).String()
 
+	isDisableTemplateFuncsSet = false
+	disableTemplateFuncs      = kingpin.Flag("disable-template-functions", "Do not use and execute any template functions in call template data. Useful for better performance").
+					Default("false").IsSetByUser(&isDisableTemplateFuncsSet).Bool()
+
+	isDisableTemplateDataSet = false
+	disableTemplateData      = kingpin.Flag("disable-template-data", "Do not use and execute any call template data. Useful for better performance.").
+					Default("false").IsSetByUser(&isDisableTemplateDataSet).Bool()
+
 	// host main argument
 	isHostSet = false
 	host      = kingpin.Arg("host", "Host and port to test.").String()
@@ -507,6 +515,8 @@ func createConfigFromArgs(cfg *runner.Config) error {
 	cfg.LBStrategy = *lbStrategy
 	cfg.MaxCallRecvMsgSize = *maxRecvMsgSize
 	cfg.MaxCallSendMsgSize = *maxSendMsgSize
+	cfg.DisableTemplateFuncs = *disableTemplateFuncs
+	cfg.DisableTemplateData = *disableTemplateData
 
 	return nil
 }
@@ -760,6 +770,16 @@ func mergeConfig(dest *runner.Config, src *runner.Config) error {
 		dest.MaxCallSendMsgSize = src.MaxCallSendMsgSize
 	}
 
+	// call data template functions behavior
+	if isDisableTemplateFuncsSet {
+		dest.DisableTemplateFuncs = src.DisableTemplateFuncs
+	}
+
+	// call data template behavior
+	if isDisableTemplateDataSet {
+		dest.DisableTemplateData = src.DisableTemplateData
+	}
+
 	return nil
 }
 
diff --git a/runner/calldata.go b/runner/calldata.go
index 06e6964..07b236c 100644
--- a/runner/calldata.go
+++ b/runner/calldata.go
@@ -50,27 +50,32 @@ var tmplFuncMap = template.FuncMap{
 	"randomInt":    randomInt,
 }
 
-var commonTemplate *template.Template = template.New("call_template_data").
-	Funcs(tmplFuncMap).
-	Funcs(template.FuncMap(sprigFuncMap))
-
 // newCallData returns new CallData
 func newCallData(
 	mtd *desc.MethodDescriptor,
-	funcs template.FuncMap,
-	workerID string, reqNum int64) *CallData {
+	workerID string, reqNum int64, withFuncs, withTemplateData bool, funcs template.FuncMap) *CallData {
+
+	var t *template.Template
+	if withTemplateData {
+		t = template.New("call_template_data")
+
+		if withFuncs {
+			t = t.
+				Funcs(tmplFuncMap).
+				Funcs(template.FuncMap(sprigFuncMap))
 
-	fns := make(template.FuncMap, len(funcs))
+			if len(funcs) > 0 {
+				fns := make(template.FuncMap, len(funcs))
 
-	if len(funcs) > 0 {
-		for k, v := range funcs {
-			fns[k] = v
+				for k, v := range funcs {
+					fns[k] = v
+				}
+
+				t = t.Funcs(fns)
+			}
 		}
 	}
 
-	t, _ := commonTemplate.Clone()
-	t.Funcs(fns)
-
 	now := time.Now()
 	newUUID, _ := uuid.NewRandom()
 
@@ -119,6 +124,10 @@ func (td *CallData) Regenerate() *CallData {
 }
 
 func (td *CallData) execute(data string) (*bytes.Buffer, error) {
+	if td.t == nil {
+		return nil, nil
+	}
+
 	t, err := td.t.Parse(data)
 	if err != nil {
 		return nil, err
@@ -134,6 +143,10 @@ func (td *CallData) execute(data string) (*bytes.Buffer, error) {
 // The *parse.Tree field is exported only for use by html/template
 // and should be treated as unexported by all other clients.
 func (td *CallData) hasAction(data string) (bool, error) {
+	if td.t == nil {
+		return false, nil
+	}
+
 	t, err := td.t.Parse(data)
 	if err != nil {
 		return false, err
@@ -165,7 +178,7 @@ func (td *CallData) ExecuteData(data string) ([]byte, error) {
 	if len(data) > 0 {
 		input := []byte(data)
 		tpl, err := td.execute(data)
-		if err == nil {
+		if err == nil && tpl != nil {
 			input = tpl.Bytes()
 		}
 
@@ -181,7 +194,7 @@ func (td *CallData) executeMetadata(metadata string) (map[string]string, error)
 	if len(metadata) > 0 {
 		input := []byte(metadata)
 		tpl, err := td.execute(metadata)
-		if err == nil {
+		if err == nil && tpl != nil {
 			input = tpl.Bytes()
 		}
 
diff --git a/runner/config.go b/runner/config.go
index a320233..b226337 100644
--- a/runner/config.go
+++ b/runner/config.go
@@ -116,6 +116,8 @@ type Config struct {
 	LBStrategy            string            `json:"lb-strategy" toml:"lb-strategy" yaml:"lb-strategy"`
 	MaxCallRecvMsgSize    string            `json:"max-recv-message-size" toml:"max-recv-message-size" yaml:"max-recv-message-size"`
 	MaxCallSendMsgSize    string            `json:"max-send-message-size" toml:"max-send-message-size" yaml:"max-send-message-size"`
+	DisableTemplateFuncs  bool              `json:"disable-template-functions" toml:"disable-template-functions" yaml:"disable-template-functions"`
+	DisableTemplateData   bool              `json:"disable-template-data" toml:"disable-template-data" yaml:"disable-template-data"`
 }
 
 func checkData(data interface{}) error {
diff --git a/runner/data.go b/runner/data.go
index ece878d..68ac133 100644
--- a/runner/data.go
+++ b/runner/data.go
@@ -65,7 +65,7 @@ type mdProvider struct {
 
 func newDataProvider(mtd *desc.MethodDescriptor,
 	binary bool, dataFunc BinaryDataFunc, data []byte,
-	funcs template.FuncMap) (*dataProvider, error) {
+	withFuncs, withTemplateData bool, funcs template.FuncMap) (*dataProvider, error) {
 
 	dp := dataProvider{
 		binary:         binary,
@@ -98,12 +98,15 @@ func newDataProvider(mtd *desc.MethodDescriptor,
 	}
 
 	// Test if we can preseed data
-	ctd := newCallData(mtd, funcs, "", 0)
 	ha := false
-	if !dp.binary {
-		ha, err = ctd.hasAction(string(dp.data))
-		if err != nil {
-			return nil, err
+	ctd := newCallData(mtd, "", 0, withFuncs, withTemplateData, funcs)
+
+	if withTemplateData {
+		if !dp.binary {
+			ha, err = ctd.hasAction(string(dp.data))
+			if err != nil {
+				return nil, err
+			}
 		}
 	}
 
@@ -221,9 +224,9 @@ func (dp *dataProvider) getMessages(ctd *CallData, i int, inputData []byte) ([]*
 	return inputs, nil
 }
 
-func newMetadataProvider(mtd *desc.MethodDescriptor, mdData []byte, funcs template.FuncMap) (*mdProvider, error) {
+func newMetadataProvider(mtd *desc.MethodDescriptor, mdData []byte, withFuncs, withTemplateData bool, funcs template.FuncMap) (*mdProvider, error) {
 	// Test if we can preseed data
-	ctd := newCallData(mtd, funcs, "", 0)
+	ctd := newCallData(mtd, "", 0, withFuncs, withTemplateData, funcs)
 	ha, err := ctd.hasAction(string(mdData))
 	if err != nil {
 		return nil, err
@@ -389,7 +392,7 @@ type dynamicMessageProvider struct {
 	indexCounter    uint
 }
 
-func newDynamicMessageProvider(mtd *desc.MethodDescriptor, data []byte, streamCallCount uint) (*dynamicMessageProvider, error) {
+func newDynamicMessageProvider(mtd *desc.MethodDescriptor, data []byte, streamCallCount uint, withFuncs, withTemplateData bool) (*dynamicMessageProvider, error) {
 	mp := dynamicMessageProvider{
 		mtd:             mtd,
 		data:            data,
@@ -419,10 +422,14 @@ func newDynamicMessageProvider(mtd *desc.MethodDescriptor, data []byte, streamCa
 	mp.arrayLen = uint(len(mp.arrayJSONData))
 
 	// Test if we have actions
-	ctd := newCallData(mtd, nil, "", 0)
-	ha, err := ctd.hasAction(string(mp.data))
-	if err != nil {
-		return nil, err
+	ha := false
+	ctd := newCallData(mtd, "", 0, withFuncs, withTemplateData, nil)
+
+	if withTemplateData {
+		ha, err = ctd.hasAction(string(mp.data))
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	if !ha {
diff --git a/runner/options.go b/runner/options.go
index 84271e2..d2d3992 100644
--- a/runner/options.go
+++ b/runner/options.go
@@ -124,6 +124,10 @@ type RunConfig struct {
 	hasLog bool
 	log    Logger
 
+	// template call data
+	disableTemplateFuncs bool
+	disableTemplateData  bool
+
 	// misc
 	name        string
 	cpus        int
@@ -1095,6 +1099,24 @@ func WithDefaultCallOptions(opts []grpc.CallOption) Option {
 	}
 }
 
+// WithDisableTemplateFuncs disables template functions in call data
+func WithDisableTemplateFuncs(v bool) Option {
+	return func(o *RunConfig) error {
+		o.disableTemplateFuncs = v
+
+		return nil
+	}
+}
+
+// WithDisableTemplateData disables template data execution in call data
+func WithDisableTemplateData(v bool) Option {
+	return func(o *RunConfig) error {
+		o.disableTemplateData = v
+
+		return nil
+	}
+}
+
 func createClientTransportCredentials(skipVerify bool, cacertFile, clientCertFile, clientKeyFile, cname string) (credentials.TransportCredentials, error) {
 	var tlsConf tls.Config
 
@@ -1187,6 +1209,8 @@ func fromConfig(cfg *Config) []Option {
 		WithConcurrencyStepDuration(time.Duration(cfg.CStepDuration)),
 		WithConcurrencyDuration(time.Duration(cfg.CMaxDuration)),
 		WithCountErrors(cfg.CountErrors),
+		WithDisableTemplateFuncs(cfg.DisableTemplateFuncs),
+		WithDisableTemplateData(cfg.DisableTemplateData),
 		func(o *RunConfig) error {
 			o.call = cfg.Call
 			return nil
diff --git a/runner/requester.go b/runner/requester.go
index 54afe85..03763c5 100644
--- a/runner/requester.go
+++ b/runner/requester.go
@@ -122,7 +122,7 @@ func NewRequester(c *RunConfig) (*Requester, error) {
 	md := mtd.GetInputType()
 	payloadMessage := dynamic.NewMessage(md)
 	if payloadMessage == nil {
-		return nil, fmt.Errorf("No input type of method: %s", mtd.GetName())
+		return nil, fmt.Errorf("no input type of method: %s", mtd.GetName())
 	}
 
 	// fill in the rest
@@ -131,7 +131,7 @@ func NewRequester(c *RunConfig) (*Requester, error) {
 	if c.dataProviderFunc != nil {
 		reqr.dataProvider = c.dataProviderFunc
 	} else {
-		defaultDataProvider, err := newDataProvider(reqr.mtd, c.binary, c.dataFunc, c.data, c.funcs)
+		defaultDataProvider, err := newDataProvider(reqr.mtd, c.binary, c.dataFunc, c.data, !c.disableTemplateFuncs, !c.disableTemplateData, c.funcs)
 		if err != nil {
 			return nil, err
 		}
@@ -141,7 +141,7 @@ func NewRequester(c *RunConfig) (*Requester, error) {
 	if c.mdProviderFunc != nil {
 		reqr.metadataProvider = c.mdProviderFunc
 	} else {
-		defaultMDProvider, err := newMetadataProvider(reqr.mtd, c.metadata, c.funcs)
+		defaultMDProvider, err := newMetadataProvider(reqr.mtd, c.metadata, !c.disableTemplateFuncs, !c.disableTemplateData, c.funcs)
 		if err != nil {
 			return nil, err
 		}
diff --git a/runner/worker.go b/runner/worker.go
index c3c7ccb..6198b4d 100644
--- a/runner/worker.go
+++ b/runner/worker.go
@@ -80,7 +80,7 @@ func (w *Worker) Stop() {
 func (w *Worker) makeRequest(tv TickValue) error {
 	reqNum := int64(tv.reqNumber)
 
-	ctd := newCallData(w.mtd, w.config.funcs, w.workerID, reqNum)
+	ctd := newCallData(w.mtd, w.workerID, reqNum, !w.config.disableTemplateFuncs, !w.config.disableTemplateData, w.config.funcs)
 
 	reqMD, err := w.metadataProvider(ctd)
 	if err != nil {
@@ -116,7 +116,7 @@ func (w *Worker) makeRequest(tv TickValue) error {
 		msgProvider = w.msgProvider
 	} else if w.mtd.IsClientStreaming() {
 		if w.config.streamDynamicMessages {
-			mp, err := newDynamicMessageProvider(w.mtd, w.config.data, w.config.streamCallCount)
+			mp, err := newDynamicMessageProvider(w.mtd, w.config.data, w.config.streamCallCount, !w.config.disableTemplateFuncs, !w.config.disableTemplateData)
 			if err != nil {
 				return err
 			}
diff --git a/www/docs/calldata.md b/www/docs/calldata.md
index 3165f46..b950977 100644
--- a/www/docs/calldata.md
+++ b/www/docs/calldata.md
@@ -123,3 +123,9 @@ report, err := runner.Run(
 	runner.WithBinaryDataFunc(dataFunc),
 )
 ```
+
+### Disabling
+
+Execution of call template functions can be done by setting `--disable-template-functions` to `true` or by using `WithDisableTemplateFuncs(true)`.
+
+Execution of call data template can be disabled completely by setting `--disable-template-data` to `true` or by using `WithDisableTemplateData(true)`.
diff --git a/www/docs/options.md b/www/docs/options.md
index e0b6bdc..ea5f050 100644
--- a/www/docs/options.md
+++ b/www/docs/options.md
@@ -390,6 +390,14 @@ Client load balancing strategy. For example: `--lb-strategy "round_robin"`
 
 By default stats for fastest, slowest, average, histogram, and latency distributions only take into account the responses with OK status. This option enabled counting of erroneous (non-OK) responses in stats calculations as well.
 
+### `--disable-template-functions`
+
+Disable execution of template functions within call data and metadata. This can be useful for some performance improvements. Note that if template functions are used within data with this option set to `true`, it will result in an error. If `--disable-template-data` is set to `true` this is automatically also set to `true`.
+
+### `--disable-template-data`
+
+Disable execution of templates within call data and metadata. This can be useful for some performance improvements. This automatically also sets `disable-template-functions` to `true`.
+
 ### `-v`, `--version`
 
 Print the version.


Test patch:
diff --git a/runner/calldata_test.go b/runner/calldata_test.go
index d00ce9b..b5404d8 100644
--- a/runner/calldata_test.go
+++ b/runner/calldata_test.go
@@ -17,7 +17,7 @@ func TestCallData_New(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotNil(t, md)
 
-	ctd := newCallData(md, nil, "worker_id_123", 100)
+	ctd := newCallData(md, "worker_id_123", 100, true, true, nil)
 
 	assert.NotNil(t, ctd)
 	assert.Equal(t, "worker_id_123", ctd.WorkerID)
@@ -73,7 +73,7 @@ func TestCallData_ExecuteData(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			ctd := newCallData(md, nil, "worker_id_123", 200)
+			ctd := newCallData(md, "worker_id_123", 200, true, true, nil)
 			assert.NotNil(t, ctd)
 
 			r, err := ctd.ExecuteData(tt.in)
@@ -124,7 +124,7 @@ func TestCallData_ExecuteMetadata(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 
-			ctd := newCallData(md, nil, "worker_id_123", 200)
+			ctd := newCallData(md, "worker_id_123", 200, true, true, nil)
 			assert.NotNil(t, ctd)
 
 			r, err := ctd.executeMetadata(tt.in)
@@ -146,7 +146,7 @@ func TestCallTemplateData_ExecuteFuncs(t *testing.T) {
 
 	t.Run("newUUID", func(t *testing.T) {
 
-		ctd := newCallData(md, nil, "worker_id_123", 200)
+		ctd := newCallData(md, "worker_id_123", 200, true, true, nil)
 		assert.NotNil(t, ctd)
 
 		// no template
@@ -194,7 +194,7 @@ func TestCallTemplateData_ExecuteFuncs(t *testing.T) {
 	})
 
 	t.Run("randomString", func(t *testing.T) {
-		ctd := newCallData(md, nil, "worker_id_123", 200)
+		ctd := newCallData(md, "worker_id_123", 200, true, true, nil)
 		assert.NotNil(t, ctd)
 
 		// no template
@@ -256,7 +256,7 @@ func TestCallTemplateData_ExecuteFuncs(t *testing.T) {
 	})
 
 	t.Run("randomInt", func(t *testing.T) {
-		ctd := newCallData(md, nil, "worker_id_123", 200)
+		ctd := newCallData(md, "worker_id_123", 200, true, true, nil)
 		assert.NotNil(t, ctd)
 
 		// no template
@@ -295,17 +295,17 @@ func TestCallTemplateData_ExecuteFuncs(t *testing.T) {
 
 	t.Run("custom functions", func(t *testing.T) {
 
-		ctd := newCallData(md, nil, "worker_id_123", 200)
+		ctd := newCallData(md, "worker_id_123", 200, true, true, nil)
 		assert.NotNil(t, ctd)
 
-		ctd = newCallData(md, template.FuncMap{
+		ctd = newCallData(md, "worker_id_123", 200, true, true, template.FuncMap{
 			"getSKU": func() string {
 				return "custom-sku"
 			},
 			"newUUID": func() string {
 				return "custom-uuid"
 			},
-		}, "worker_id_123", 200)
+		})
 
 		r, err := ctd.ExecuteData(`{"trace_id":"{{newUUID}}", "span_id":"{{getSKU}}"}`)
 		assert.NoError(t, err)
@@ -320,7 +320,7 @@ func TestCallTemplateData_ExecuteFuncs(t *testing.T) {
 
 	t.Run("sprig functions", func(t *testing.T) {
 
-		ctd := newCallData(md, nil, "worker_id_123", 200)
+		ctd := newCallData(md, "worker_id_123", 200, true, true, nil)
 		assert.NotNil(t, ctd)
 
 		r, err := ctd.ExecuteData(`{"trace_id":"{{add 1 2}}"}`)
diff --git a/runner/data_test.go b/runner/data_test.go
index 0995780..463635e 100644
--- a/runner/data_test.go
+++ b/runner/data_test.go
@@ -275,7 +275,7 @@ func TestMetadata_newMetadataProvider(t *testing.T) {
 			nil)
 		assert.NoError(t, err)
 
-		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"asdf"}`), nil)
+		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"asdf"}`), true, true, nil)
 		assert.NoError(t, err)
 		assert.NotNil(t, mdp)
 		assert.NotNil(t, mdp.preseed)
@@ -289,7 +289,7 @@ func TestMetadata_newMetadataProvider(t *testing.T) {
 			nil)
 		assert.NoError(t, err)
 
-		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"{{ .RequestNumber }}"}`), nil)
+		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"{{ .RequestNumber }}"}`), true, true, nil)
 		assert.NoError(t, err)
 		assert.NotNil(t, mdp)
 		assert.Nil(t, mdp.preseed)
@@ -304,11 +304,11 @@ func TestMetadata_getMetadataForCall(t *testing.T) {
 			nil)
 		assert.NoError(t, err)
 
-		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"asdf"}`), nil)
+		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"asdf"}`), true, true, nil)
 		assert.NoError(t, err)
 		assert.NotNil(t, mdp.preseed)
 
-		cd := newCallData(mtdUnary, nil, "123", 1)
+		cd := newCallData(mtdUnary, "123", 1, true, true, nil)
 
 		md, err := mdp.getMetadataForCall(cd)
 		assert.NoError(t, err)
@@ -324,11 +324,11 @@ func TestMetadata_getMetadataForCall(t *testing.T) {
 			nil)
 		assert.NoError(t, err)
 
-		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"{{ .RequestNumber }}"}`), nil)
+		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"{{ .RequestNumber }}"}`), true, true, nil)
 		assert.NoError(t, err)
 		assert.Nil(t, mdp.preseed)
 
-		cd := newCallData(mtdUnary, nil, "123", 1)
+		cd := newCallData(mtdUnary, "123", 1, true, true, nil)
 
 		md1, err := mdp.getMetadataForCall(cd)
 		assert.NoError(t, err)
@@ -336,7 +336,7 @@ func TestMetadata_getMetadataForCall(t *testing.T) {
 		assert.Equal(t, []string{"1"}, md1.Get("token"))
 		assert.NotSame(t, mdp.preseed, md1)
 
-		cd = newCallData(mtdUnary, nil, "123", 2)
+		cd = newCallData(mtdUnary, "123", 2, true, true, nil)
 		md2, err := mdp.getMetadataForCall(cd)
 		assert.NoError(t, err)
 		assert.NotNil(t, md2)
@@ -359,11 +359,11 @@ func TestMetadata_getMetadataForCall(t *testing.T) {
 			},
 		}
 
-		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"{{ customFunc }}"}`), funcs)
+		mdp, err := newMetadataProvider(mtdUnary, []byte(`{"token":"{{ customFunc }}"}`), true, true, funcs)
 		assert.NoError(t, err)
 		assert.Nil(t, mdp.preseed)
 
-		cd := newCallData(mtdUnary, funcs, "123", 1)
+		cd := newCallData(mtdUnary, "123", 1, true, true, funcs)
 
 		md1, err := mdp.getMetadataForCall(cd)
 		assert.NoError(t, err)
diff --git a/runner/run_test.go b/runner/run_test.go
index b60f233..33927df 100644
--- a/runner/run_test.go
+++ b/runner/run_test.go
@@ -3,6 +3,7 @@ package runner
 import (
 	"fmt"
 	"strconv"
+	"strings"
 	"testing"
 	"text/template"
 	"time"
@@ -217,6 +218,93 @@ func TestRunUnary(t *testing.T) {
 		assert.NotEmpty(t, parsed)
 	})
 
+	t.Run("test disabled template functions", func(t *testing.T) {
+		gs.ResetCounters()
+
+		data := make(map[string]interface{})
+		data["name"] = "{{ newUUID }}"
+
+		report, err := Run(
+			"helloworld.Greeter.SayHello",
+			internal.TestLocalhost,
+			WithProtoFile("../testdata/greeter.proto", []string{}),
+			WithTotalRequests(1),
+			WithConcurrency(1),
+			WithTimeout(time.Duration(20*time.Second)),
+			WithData(data),
+			WithInsecure(true),
+			WithDisableTemplateFuncs(true),
+		)
+
+		assert.Error(t, err)
+		assert.True(t, strings.Contains(err.Error(), `function "newUUID" not defined`))
+		assert.Nil(t, report)
+	})
+
+	t.Run("test disabled template functions with data", func(t *testing.T) {
+		gs.ResetCounters()
+
+		data := make(map[string]interface{})
+		data["name"] = "{{ .RequestNumber }}"
+
+		report, err := Run(
+			"helloworld.Greeter.SayHello",
+			internal.TestLocalhost,
+			WithProtoFile("../testdata/greeter.proto", []string{}),
+			WithTotalRequests(1),
+			WithConcurrency(1),
+			WithTimeout(time.Duration(20*time.Second)),
+			WithData(data),
+			WithInsecure(true),
+			WithDisableTemplateFuncs(true),
+		)
+
+		assert.NoError(t, err)
+		assert.NotNil(t, report)
+
+		count := gs.GetCount(callType)
+		assert.Equal(t, 1, count)
+
+		calls := gs.GetCalls(callType)
+		assert.NotNil(t, calls)
+		assert.Len(t, calls, 1)
+
+		msg := calls[0][0]
+		assert.Equal(t, "0", msg.GetName())
+	})
+
+	t.Run("test disabled template data", func(t *testing.T) {
+		gs.ResetCounters()
+
+		data := make(map[string]interface{})
+		data["name"] = "{{ .RequestNumber }}"
+
+		report, err := Run(
+			"helloworld.Greeter.SayHello",
+			internal.TestLocalhost,
+			WithProtoFile("../testdata/greeter.proto", []string{}),
+			WithTotalRequests(1),
+			WithConcurrency(1),
+			WithTimeout(time.Duration(20*time.Second)),
+			WithData(data),
+			WithInsecure(true),
+			WithDisableTemplateData(true),
+		)
+
+		assert.NoError(t, err)
+		assert.NotNil(t, report)
+
+		count := gs.GetCount(callType)
+		assert.Equal(t, 1, count)
+
+		calls := gs.GetCalls(callType)
+		assert.NotNil(t, calls)
+		assert.Len(t, calls, 1)
+
+		msg := calls[0][0]
+		assert.Equal(t, "{{ .RequestNumber }}", msg.GetName())
+	})
+
 	t.Run("test skip first N", func(t *testing.T) {
 		gs.ResetCounters()