# Request: Ability to Fire Component Events using String Payloads

**URL:** <https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911>\
**Category:** Module Development\
**Tags:** perspective\
**Created:** [June 29, 2025, 3:44am UTC](https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911 "2025-06-29T03:44:53Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [June 29, 2025, 3:44am UTC](https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911/1 "2025-06-29T03:44:53Z")

</div>

When sending large messages containing user-provided Python content to Perspective components/pages, the overhead of creating a single use `JsonObject` can be quite large, just for it to be immediately serialized into a string.

## Test

```python
# The message body to be sent.
message = {
	'param1': [{ "items": [j for j in range(40)] } for i in range(1000) ]
}

```

### Method 1

Create a `JsonObject`, and use `TypeUtilities.pyToGson` to convert the `message` parameter.  
Send the message via [`PageModel.send`](https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.33/com/inductiveautomation/perspective/gateway/model/PageModel.html#send(java.lang.String,com.inductiveautomation.ignition.common.gson.JsonElement)) using the `JsonElement` overload.

### Method 2

Use a `JsonWriter` and [`SystemUtilities.jsonEncode`](https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.33/com/inductiveautomation/ignition/common/script/builtin/SystemUtilities.html#jsonEncode(org.python.core.PyObject)) to directly create the message as a string.  
Send the message via [`PageMode.send`](https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.33/com/inductiveautomation/perspective/gateway/model/PageModel.html#send(java.lang.String,java.lang.String)) using the `String` overload.

| Method | Time To Serialize + Send Message |
| --- | --- |
| Intermediate `JsonObject` | **~190ms** |
| `JsonWriter` | **~6ms** |

## My Request

There is no [`ComponentModelDelegate.fireEvent`](https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.33/com/inductiveautomation/perspective/gateway/api/ComponentModelDelegate.html#fireEvent(java.lang.String,com.inductiveautomation.ignition.common.gson.JsonObject)) overload that takes a string. You _have_ to provide a `JsonObject`.

Can we get an overload that takes a string?

I can make an Idea's Portal request if it's important, but I think this is too niche to get any traction there.

Edit: Eh, maybe I requested too early in my though process.

You can get the same performance boost by using `SystemUtilities.jsonEncode` to add the message to the `JsonObject`, then using `JSON.parse` on the client. So maybe this is really a PSA that `SystemUtilities.jsonEncode` is _much_ faster than `TypeUtilities.pyToGson` 🤷‍♂️.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [June 30, 2025, 5:31pm UTC](https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911/2 "2025-06-30T17:31:52Z")

</div>

> [@bmusson](#):
>
> So maybe this is really a PSA that `SystemUtilities.jsonEncode` is _much_ faster than `TypeUtilities.pyToGson` 🤷‍♂️.

My guess is that the significant difference is because it's not apples to apples.  
`pyToGson` has to allocate the thousands of individual `JsonElement`s internally and return them to you as a rich object graph. `jsonEncode` gets to just spit out strings - no extra allocation churn required.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [June 30, 2025, 5:43pm UTC](https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911/3 "2025-06-30T17:43:25Z")

</div>

Although, maybe it's not _just_ that...  
Even using a JsonWriter directly, which is fairly unpleasant, and avoiding intermediate allocations, it's still a lot slower:

```python
Benchmark Mode Cnt Score Error Units
PyJsonBenchmark.customPyToGson avgt 10 47.964 ± 2.301 ms/op
PyJsonBenchmark.jsonEncode avgt 10 2.207 ± 0.262 ms/op
PyJsonBenchmark.pyToGson avgt 10 67.329 ± 3.034 ms/op

```

Dunno.

> **JMH Test**
>
> ```java
> package com.inductiveautomation.common.script.bench;
> 
> import java.io.IOException;
> import java.io.Writer;
> import java.util.concurrent.TimeUnit;
> import javax.annotation.Nullable;
> 
> import com.inductiveautomation.ignition.common.PyUtilities;
> import com.inductiveautomation.ignition.common.TypeUtilities;
> import com.inductiveautomation.ignition.common.gson.Gson;
> import com.inductiveautomation.ignition.common.gson.stream.JsonWriter;
> import com.inductiveautomation.ignition.common.script.builtin.SystemUtilities;
> import org.apache.commons.io.output.StringBuilderWriter;
> import org.intellij.lang.annotations.Language;
> import org.json.JSONException;
> import org.openjdk.jmh.annotations.Benchmark;
> import org.openjdk.jmh.annotations.BenchmarkMode;
> import org.openjdk.jmh.annotations.Fork;
> import org.openjdk.jmh.annotations.Measurement;
> import org.openjdk.jmh.annotations.Mode;
> import org.openjdk.jmh.annotations.OutputTimeUnit;
> import org.openjdk.jmh.annotations.Scope;
> import org.openjdk.jmh.annotations.Setup;
> import org.openjdk.jmh.annotations.State;
> import org.openjdk.jmh.annotations.Threads;
> import org.openjdk.jmh.annotations.Warmup;
> import org.openjdk.jmh.runner.Runner;
> import org.openjdk.jmh.runner.RunnerException;
> import org.openjdk.jmh.runner.options.Options;
> import org.openjdk.jmh.runner.options.OptionsBuilder;
> import org.python.core.CompileMode;
> import org.python.core.CompilerFlags;
> import org.python.core.Py;
> import org.python.core.PyCode;
> import org.python.core.PyObject;
> import org.python.core.PyStringMap;
> 
> @BenchmarkMode(Mode.AverageTime)
> @OutputTimeUnit(TimeUnit.MILLISECONDS)
> @Warmup(iterations = 10, time = 1)
> @Measurement(iterations = 10, time = 1)
> @Fork(1)
> @Threads(1)
> public class PyJsonBenchmark {
> 
> @State(Scope.Benchmark)
> public static class BenchmarkState {
> PyObject list;
> 
> final Gson gson = new Gson();
> 
> @Setup
> public void setup() {
> @Language("python")
> String script = """
> {
> 'param1': [{ "items": [j for j in xrange(40)] } for i in xrange(1000) ]
> }
> """;
> list = eval(script);
> }
> 
> private PyObject eval(@Language("python") String script) {
> PyStringMap globals = exec(Py.newString("__RESULT") + " = " + script);
> return globals.get(Py.newString("__RESULT"));
> }
> 
> private PyStringMap exec(@Language("python") String script) {
> PyCode compiledCall = compile("<test>", script);
> PyStringMap globals = new PyStringMap();
> Py.runCode(compiledCall, null, globals);
> return globals;
> }
> 
> private PyCode compile(String filename, @Language("python") String script) {
> return Py.compile_flags(
> script,
> filename,
> CompileMode.exec,
> CompilerFlags.getCompilerFlags().combine(CompilerFlags.PyCF_SOURCE_IS_UTF8)
> );
> }
> }
> 
> @Benchmark
> public String jsonEncode(BenchmarkState state) throws JSONException {
> return SystemUtilities.jsonEncode(state.list);
> }
> 
> @Benchmark
> public String pyToGson(BenchmarkState state) {
> return TypeUtilities.pyToGson(state.list).toString();
> }
> 
> @Benchmark
> public String customPyToGson(BenchmarkState state) throws IOException {
> Writer stringWriter = new StringBuilderWriter();
> try (JsonWriter writer = new JsonWriter(stringWriter)) {
> pyToGsonWriter(state.list, writer, state.gson);
> }
> return stringWriter.toString();
> }
> 
> public static void pyToGsonWriter(@Nullable PyObject pyObject, JsonWriter writer, Gson gson) throws IOException {
> if (pyObject == null || Py.None.equals(pyObject)) {
> writer.nullValue();
> return;
> }
> 
> if (PyUtilities.isSequence(pyObject)) {
> writer.beginArray();
> for (PyObject item : pyObject.asIterable()) {
> pyToGsonWriter(item, writer, gson);
> }
> writer.endArray();
> return;
> } else if (pyObject.isMappingType()) {
> writer.beginObject();
> for (PyObject key : pyObject.asIterable()) {
> writer.name(key.toString());
> pyToGsonWriter(pyObject. __getitem__ (key), writer, gson);
> }
> writer.endObject();
> return;
> } else if (PyUtilities.isIterable(pyObject) && !PyUtilities.isString(pyObject)) {
> writer.beginArray();
> for (PyObject item : pyObject.asIterable()) {
> pyToGsonWriter(item, writer, gson);
> }
> writer.endArray();
> return;
> }
> 
> Object o = pyObject. __tojava__ (Object.class);
> if (o instanceof String string) {
> writer.value(string);
> } else if (o instanceof Number number) {
> writer.value(number);
> } else if (o instanceof Boolean bool) {
> writer.value(bool);
> } else if (o == null) {
> writer.nullValue();
> } else {
> gson.toJson(o, o.getClass(), writer);
> }
> }
> 
> public static void main(String[] args) throws RunnerException {
> Options opt = new OptionsBuilder()
> .include(PyJsonBenchmark.class.getSimpleName())
> .build();
> 
> new Runner(opt).run();
> }
> }
> 
> ```

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [June 30, 2025, 6:17pm UTC](https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911/4 "2025-06-30T18:17:15Z")

</div>

> [@paul-griffith](#):
>
> My guess is that the significant difference is because it's not apples to apples.

100%, I was talking specifically in the context of preparing Perspective messages where the goal is to immediately serialize to a string for sending over the WebSocket.

I knew that building an single-use `JsonObject` had to be slower, but I didn't realize how much performance was left on the table.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [June 30, 2025, 8:52pm UTC](https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911/5 "2025-06-30T20:52:47Z")

</div>

For what it's worth,  
`ComponentModelDelegate.fireEvent` is just a simple cover method:

```python
    public void fireEvent(String eventName, JsonObject event) {
        component.fireEvent(EventConfig.MODEL_EVENTS, eventName, event);
    }

```

And the underlying `fireEvent` is accepting `Object`. Now, we're still going to rinse that through JSON, so you may not actually gain anything, but you could in theory pass a raw string there directly?

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [June 30, 2025, 9:27pm UTC](https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911/6 "2025-06-30T21:27:15Z")

</div>

Huh. So, I found out why (besides the allocation) it's so much slower:

```python
Benchmark Mode Cnt Score Error Units
PyJsonBenchmark.customPyToGson avgt 10 3.835 ± 0.079 ms/op
PyJsonBenchmark.jsonEncode avgt 10 2.107 ± 0.034 ms/op
PyJsonBenchmark.pyToGson avgt 10 66.651 ± 1.742 ms/op

```

The culprit is `PyUtilities#isIterable`, which, because duck typing, relies on exception catching around the ` __iter__ ` dunder method:

```java
    public static boolean isIterable(PyObject pyObject) {
        try {
            pyObject. __iter__ ();
            return true;
        } catch (Exception ignored) {
            return false;
        }
    }

```

Which is naturally quite slow, because (among other things) it's building a real stack trace.

If you just drop that check to handle unbounded sequences/generator expressions in the `pyToGson` duplicate:

```java

    @Benchmark
    public String customPyToGson(BenchmarkState state) throws IOException {
        Writer stringWriter = new StringBuilderWriter();
        try (JsonWriter writer = new JsonWriter(stringWriter)) {
            pyToGsonWriter(state.list, writer, state.gson);
        }
        return stringWriter.toString();
    }

    public static void pyToGsonWriter(@Nullable PyObject pyObject, JsonWriter writer, Gson gson) throws IOException {
        if (pyObject == null || Py.None.equals(pyObject)) {
            writer.nullValue();
            return;
        }

        if (PyUtilities.isSequence(pyObject)) {
            writer.beginArray();
            for (PyObject item : pyObject.asIterable()) {
                pyToGsonWriter(item, writer, gson);
            }
            writer.endArray();
            return;
        } else if (pyObject.isMappingType()) {
            writer.beginObject();
            for (PyObject key : pyObject.asIterable()) {
                writer.name(key.toString());
                pyToGsonWriter(pyObject. __getitem__ (key), writer, gson);
            }
            writer.endObject();
            return;
        }
        // } else if (PyUtilities.isIterable(pyObject) && !PyUtilities.isString(pyObject)) {
        // writer.beginArray();
        // for (PyObject item : pyObject.asIterable()) {
        // pyToGsonWriter(item, writer, gson);
        // }
        // writer.endArray();
        // return;
        // }

        Object o = pyObject. __tojava__ (Object.class);
        if (o instanceof String string) {
            writer.value(string);
        } else if (o instanceof Number number) {
            writer.value(number);
        } else if (o instanceof Boolean bool) {
            writer.value(bool);
        } else if (o == null) {
            writer.nullValue();
        } else {
            gson.toJson(o, o.getClass(), writer);
        }
    }

```

You get the results at the beginning. Might be worth keeping around in a back pocket 🤷

---

<div class="post-metadata">

**Author:** ![Leor\_Fishman](https://avatars.discourse-cdn.com/v4/letter/l/e19adc/32.png) [@Leor\_Fishman](https://forum.inductiveautomation.com/u/Leor_Fishman)\
**Post date:** [June 30, 2025, 11:10pm UTC](https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911/7 "2025-06-30T23:10:52Z")

</div>

> When sending large messages containing user-provided Python content to Perspective components/pages

ok now I _have_ to know why this is a feature you're playing with 🙂

(Also, I wonder if you could manually construct the json object with allocators?)

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [July 1, 2025, 12:16am UTC](https://forum.inductiveautomation.com/t/request-ability-to-fire-component-events-using-string-payloads/107911/8 "2025-07-01T00:16:52Z")

</div>

> [@Leor\_Fishman](#):
>
> ok now I _have_ to know why this is a feature you're playing with 🙂

Nothing new, just looking to eke out some more performance from Embr's [`system.perspective.runJavaScript`](https://docs.mussonindustrial.com/ignition/embr-periscope/scripting/perspective/system-perspective-runJavaScriptAsync) functions and [`JavaScriptProxy`](https://docs.mussonindustrial.com/ignition/embr-core/javascript-proxy/) objects.

Both features allow users to supply dictionaries of parameters along with their JavaScript functions, and if you're trying to [send data in chunks for Embr Charts](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/87), every millisecond counts.
