diff --git a/.github/workflows/ci.gd.yml b/.github/workflows/ci.gd.yml index f7c2871..685b0ac 100644 --- a/.github/workflows/ci.gd.yml +++ b/.github/workflows/ci.gd.yml @@ -29,6 +29,8 @@ jobs: - name: Run tests run: cd trimsock.gd && sh/test.sh perf: + needs: + - test strategy: fail-fast: false matrix: diff --git a/.github/workflows/ci.js.yml b/.github/workflows/ci.js.yml index f99a4cb..651e49f 100644 --- a/.github/workflows/ci.js.yml +++ b/.github/workflows/ci.js.yml @@ -40,4 +40,14 @@ jobs: run: cd trimsock.js && bun install - name: Test run: cd trimsock.js && bun test - + perf: + runs-on: ubuntu-latest + needs: + - test + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - name: Install + run: cd trimsock.js && bun install + - name: Test + run: cd trimsock.js && bun perf diff --git a/trimsock.gd/addons/trimsock.gd/command.gd b/trimsock.gd/addons/trimsock.gd/command.gd index d0b2521..27fe53b 100644 --- a/trimsock.gd/addons/trimsock.gd/command.gd +++ b/trimsock.gd/addons/trimsock.gd/command.gd @@ -4,7 +4,7 @@ class_name TrimsockCommand class Chunk: var text: String var is_quoted: bool - + static func quoted(p_text: String) -> Chunk: var chunk := Chunk.new() chunk.is_quoted = true @@ -16,7 +16,7 @@ class Chunk: chunk.is_quoted = false chunk.text = p_text return chunk - + static func of_text(p_text: String) -> Chunk: var chunk := Chunk.new() chunk.is_quoted = p_text.contains(" ") @@ -65,7 +65,7 @@ static func simple(name: String, text: String = "") -> TrimsockCommand: command.name = name if text: command.chunks.append(Chunk.of_text(text)) - + return command static func request(name: String, exchange_id: String = "") -> TrimsockCommand: @@ -105,20 +105,20 @@ static func stream_finish(name: String, exchange_id: String = "") -> TrimsockCom static func error_from(command: TrimsockCommand, name: String, data) -> TrimsockCommand: var result := TrimsockCommand.new() - + if not result.is_simple(): result.name = "" result.type = Type.ERROR_RESPONSE result.exchange_id = command.exchange_id else: result.name = name - + if typeof(data) == TYPE_ARRAY: for param in data: result.params.append(str(param)) else: result.chunks.append(Chunk.of_text(str(data))) - + return result static func unescape(what: String) -> String: @@ -285,7 +285,7 @@ func serialize_to_stream(out: StreamPeer) -> void: if is_empty() and not is_raw: out.put_u8(_ord("\n")) return - + # Space after name out.put_u8(_ord(" ")) @@ -308,11 +308,11 @@ func serialize_to_stream(out: StreamPeer) -> void: elif not kv_pairs.is_empty() or not kv_map.is_empty() or not params.is_empty(): # Fall back to params if no chunks var tokens := PackedStringArray() - + # Print params first for param in params: tokens.append(_autoquoted_chunk(param)) - + # Print kv-params, either from `kv_pairs`, or `kv_map` if not kv_pairs.is_empty(): for pair in kv_pairs: @@ -340,10 +340,10 @@ func equals(what) -> bool: if not command.name == name or \ not command.type == type: return false - + if not is_simple() and exchange_id != command.exchange_id: return false - + if not is_raw: return text == command.text else: diff --git a/trimsock.gd/addons/trimsock.gd/plugin.cfg b/trimsock.gd/addons/trimsock.gd/plugin.cfg index 14bb659..3e8a335 100644 --- a/trimsock.gd/addons/trimsock.gd/plugin.cfg +++ b/trimsock.gd/addons/trimsock.gd/plugin.cfg @@ -3,5 +3,5 @@ name="trimsock.gd" description="Godot implementation of the trimsock protocol" author="Tamás Gálffy" -version="0.13.0" +version="0.13.1" script="trimsock.gd" diff --git a/trimsock.gd/addons/trimsock.gd/reader.gd b/trimsock.gd/addons/trimsock.gd/reader.gd index 48d00d4..d3fe7c2 100644 --- a/trimsock.gd/addons/trimsock.gd/reader.gd +++ b/trimsock.gd/addons/trimsock.gd/reader.gd @@ -23,11 +23,11 @@ func _pop() -> TrimsockCommand: var data_size := int(_queued_raw.text) if not _line_reader.has_data(data_size): return null - + _queued_raw.raw = _line_reader.read_data(data_size) _queued_raw.text = "" _queued_raw.chunks.clear() - + var result := _queued_raw _queued_raw = null return result @@ -36,13 +36,13 @@ func _pop() -> TrimsockCommand: var line := _line_reader.read_text() if not line: return null - + var command := _line_parser.parse(line) if command.is_raw: # Command is raw, we'll keep it in the queue until we read the binary # data for it _queued_raw = command - + # Try getting it immediately, in case we already have the data in buffer return _pop() diff --git a/trimsock.gd/tests/command.perf.gd b/trimsock.gd/tests/command.perf.gd new file mode 100644 index 0000000..c210688 --- /dev/null +++ b/trimsock.gd/tests/command.perf.gd @@ -0,0 +1,27 @@ +extends VestTest + +var commands := [ + TrimsockCommand.request("whereami"), + TrimsockCommand.request("session/set-game-id").with_params(["RghrnyJK0mUA7cW05fCKo"]), + TrimsockCommand.request("lobby/create").with_params([ "noray://example.com:8890/Q9VKjXiAlwVK" ]).with_kv_map({ "name": "Cool Lobby" }), + TrimsockCommand.request("lobby/lock").with_text("YuDqpQovXvpc"), + TrimsockCommand.request("lobby/delete").with_text("YuDqpQovXvpc") +] as Array[TrimsockCommand] + +func get_suite_name(): + return "TrimsockCommand" + +func suite(): + test("Serialization", func(): + var idx := 0 + var peer := StreamPeerBuffer.new() + + benchmark("Predefined commands", func(__): + commands[idx].serialize_to_stream(peer) + peer.data_array.clear() + idx = (idx + 1) % commands.size() + )\ + .with_duration(4.0)\ + .with_batch_size(512)\ + .run() + ) diff --git a/trimsock.gd/tests/command.perf.gd.uid b/trimsock.gd/tests/command.perf.gd.uid new file mode 100644 index 0000000..07c6ce5 --- /dev/null +++ b/trimsock.gd/tests/command.perf.gd.uid @@ -0,0 +1 @@ +uid://dskylbpiwu1nd diff --git a/trimsock.gd/tests/command.test.gd b/trimsock.gd/tests/command.test.gd index 311d2af..e684ead 100644 --- a/trimsock.gd/tests/command.test.gd +++ b/trimsock.gd/tests/command.test.gd @@ -1,7 +1,7 @@ extends VestTest func get_suite_name(): - return "Command" + return "TrimsockCommand" func suite(): define("serialize", func(): @@ -11,23 +11,23 @@ func suite(): check_serialized("raw", TrimsockCommand.simple("command").as_raw(), "\rcommand 0\n\n") check_serialized("raw quoted", TrimsockCommand.simple("command name").as_raw(), "\r\"command name\" 0\n\n") ) - + define("request-response", func(): check_serialized("request", TrimsockCommand.request("command", "1234"), "command?1234\n") check_serialized("success", TrimsockCommand.success_response("command", "1234"), "command.1234\n") check_serialized("error", TrimsockCommand.error_response("command", "1234"), "command!1234\n") check_serialized("stream", TrimsockCommand.stream_chunk("command", "1234"), "command|1234\n") - + check_serialized("success without name", TrimsockCommand.success_response("", "1234"), ".1234\n") check_serialized("error without name", TrimsockCommand.error_response("", "1234"), "!1234\n") check_serialized("stream without name", TrimsockCommand.stream_chunk("", "1234"), "|1234\n") ) - + define("multiparam", func(): check_serialized("simple params", TrimsockCommand.simple("command").with_params(["foo", "bar"]), "command foo bar\n") check_serialized("quote params", TrimsockCommand.simple("command").with_params(["foo bar", "quix"]), "command \"foo bar\" quix\n") ) - + define("kv-params", func(): check_serialized("simple", TrimsockCommand.simple("command").with_kv_pairs([TrimsockCommand.pair_of("foo", "bar")]), "command foo=bar\n") check_serialized("quoted", TrimsockCommand.simple("command").with_kv_pairs([TrimsockCommand.pair_of("foo bar", "quix baz")]), "command \"foo bar\"=\"quix baz\"\n") @@ -35,7 +35,7 @@ func suite(): check_serialized("value-quoted", TrimsockCommand.simple("command").with_kv_pairs([TrimsockCommand.pair_of("foo", "quix baz")]), "command foo=\"quix baz\"\n") check_serialized("with params", TrimsockCommand.simple("command").with_kv_pairs([TrimsockCommand.pair_of("foo", "bar")]).with_params(["foo", "bar"]), "command foo bar foo=bar\n") ) - + define("kv-map", func(): check_serialized("simple", TrimsockCommand.simple("command").with_kv_map({ "foo": "bar" }), "command foo=bar\n") check_serialized("quoted", TrimsockCommand.simple("command").with_kv_map({ "foo bar": "quix baz"}), "command \"foo bar\"=\"quix baz\"\n") diff --git a/trimsock.gd/tests/reactor.test.gd b/trimsock.gd/tests/reactor.test.gd index fb92864..a58e468 100644 --- a/trimsock.gd/tests/reactor.test.gd +++ b/trimsock.gd/tests/reactor.test.gd @@ -20,38 +20,38 @@ func suite(): var handler := func(cmd, xchg): commands.append(cmd) reactor.on("command", handler) - + reactor.ingest_text(some_source, "command foo\n") reactor.poll() - + expect_not_empty(commands) ) - + test("should handle unknown", func(): var commands := [] var handler := func(cmd, xchg): commands.append(cmd) reactor.send(some_source, TrimsockCommand.simple("error")) reactor.on_unknown(handler) - + reactor.ingest_text(some_source, "unknown foo\n") reactor.poll() - + expect_not_empty(commands, "No commands handled!") expect_not_empty(reactor.outbox, "No commands sent!") ) - + test("should handle unknown by default", func(): # Flimsy test: it should ensure that the default handler is not broken # The test only breaks when ran in debug, but might catch an issue or # two simply by being here - + reactor.ingest_text(some_source, "unknown foo\n") reactor.poll() - + ok() ) - + test("should route to exchange", func(): var commands := [] var handler := func(cmd: TrimsockCommand, xchg: TrimsockExchange): @@ -59,23 +59,23 @@ func suite(): while xchg.is_open(): commands.append(await xchg.read()) reactor.on("command", handler) - + reactor.ingest_text(some_source, "command|1 foo\ncommand|1 bar\n") reactor.poll() - + expect_equal(commands.size(), 2) ) - + test("should reply with handler return value", func(): var commands := [] var handler := func(cmd, xchg): commands.append(cmd) return TrimsockCommand.simple("response") reactor.on("command", handler) - + reactor.ingest_text(some_source, "command foo\n") reactor.poll() - + expect_equal(commands.size(), 1) expect_equal(reactor.outbox[0].target, some_source) expect_equal(reactor.outbox[0].command, TrimsockCommand.simple("response")) @@ -84,18 +84,18 @@ func suite(): test("should fill ID on request", func(): var command := TrimsockCommand.simple("request") command.exchange_id = "" - + reactor.request(some_source, command) - + expect(reactor.outbox[0].command.is_request(), "Command was not a request!") expect_not_empty(reactor.outbox[0].command.exchange_id, "Request ID was empty!") ) - test("should fill ID on stream", func(): + test("should fill ID on stream", func(): var command := TrimsockCommand.simple("stream", "foo") command.exchange_id = "" - + reactor.stream(some_source, command) - + expect(reactor.outbox[0].command.is_stream(), "Command was not a stream!") expect_not_empty(reactor.outbox[0].command.exchange_id, "Stream ID was empty!") ) diff --git a/trimsock.gd/tests/reader.perf.gd b/trimsock.gd/tests/reader.perf.gd new file mode 100644 index 0000000..26b5079 --- /dev/null +++ b/trimsock.gd/tests/reader.perf.gd @@ -0,0 +1,28 @@ +extends VestTest + +const COMMANDS := [ + "whereami?\n", + "session/set-game-id RghrnyJK0mUA7cW05fCKo\n", + "lobby/create? noray://example.com:8890/Q9VKjXiAlwVK name=\"Cool Lobby\"\n", + "lobby/lock? YuDqpQovXvpc", + "lobby/delete? YuDqpQovXvpc" +] + +func get_suite_name(): + return "TrimsockReader" + +func suite(): + var reader := TrimsockReader.new() + var idx := 0 + + test("Parsing", func(): + benchmark("Predefined commands", func(__): + reader.ingest_text(COMMANDS[idx]) + while reader.read() != null: + pass + idx = (idx + 1) % COMMANDS.size() + )\ + .with_duration(4.)\ + .with_batch_size(128)\ + .run() + ) diff --git a/trimsock.gd/tests/reader.perf.gd.uid b/trimsock.gd/tests/reader.perf.gd.uid new file mode 100644 index 0000000..3e43c8c --- /dev/null +++ b/trimsock.gd/tests/reader.perf.gd.uid @@ -0,0 +1 @@ +uid://brvun1xvnsvqx diff --git a/trimsock.gd/tests/reader.test.gd b/trimsock.gd/tests/reader.test.gd index 6e0cbc4..98ce24e 100644 --- a/trimsock.gd/tests/reader.test.gd +++ b/trimsock.gd/tests/reader.test.gd @@ -1,7 +1,7 @@ extends VestTest func get_suite_name(): - return "Reader" + return "TrimsockReader" var reader: TrimsockReader @@ -9,7 +9,7 @@ func suite(): on_case_begin.connect(func(__): reader = TrimsockReader.new() ) - + test("should read raw message", func(): ok() reader.ingest_text("\rcommand 4\na\ncd\n") diff --git a/trimsock.js/bun.lockb b/trimsock.js/bun.lockb index 5519bd9..9fcbd89 100644 Binary files a/trimsock.js/bun.lockb and b/trimsock.js/bun.lockb differ diff --git a/trimsock.js/package.json b/trimsock.js/package.json index 8cd4b44..72001fe 100644 --- a/trimsock.js/package.json +++ b/trimsock.js/package.json @@ -1,13 +1,14 @@ { "name": "trimsock", "private": true, - "version": "0.18.1", + "version": "0.18.2", "type": "module", "scripts": { "docs": "typedoc", "format": "bun --filter '*' check --write --unsafe", "fmt": "bun format", - "build": "bun --filter '*' build" + "build": "bun --filter '*' build", + "perf": "bun --filter '*' perf" }, "workspaces": [ "packages/*", diff --git a/trimsock.js/packages/trimsock-bun/package.json b/trimsock.js/packages/trimsock-bun/package.json index bc2ab59..f345475 100644 --- a/trimsock.js/packages/trimsock-bun/package.json +++ b/trimsock.js/packages/trimsock-bun/package.json @@ -1,6 +1,6 @@ { "name": "@foxssake/trimsock-bun", - "version": "0.18.1", + "version": "0.18.2", "module": "dist/index.js", "type": "module", "author": "Tamás Gálffy", diff --git a/trimsock.js/packages/trimsock-js/lib/reader.ts b/trimsock.js/packages/trimsock-js/lib/reader.ts index a923963..a7eacda 100644 --- a/trimsock.js/packages/trimsock-js/lib/reader.ts +++ b/trimsock.js/packages/trimsock-js/lib/reader.ts @@ -250,7 +250,7 @@ export class TrimsockReader { } /** - * Try and extract a command from the ingested ata + * Try and extract a command from the ingested data * * @returns the extracted command, or undefined */ diff --git a/trimsock.js/packages/trimsock-js/package.json b/trimsock.js/packages/trimsock-js/package.json index 8b1e903..72d0196 100644 --- a/trimsock.js/packages/trimsock-js/package.json +++ b/trimsock.js/packages/trimsock-js/package.json @@ -1,6 +1,6 @@ { "name": "@foxssake/trimsock-js", - "version": "0.18.1", + "version": "0.18.2", "module": "dist/index.js", "type": "module", "author": "Tamás Gálffy", @@ -9,11 +9,14 @@ "format": "biome format", "lint": "biome lint", "check": "biome check", - "build": "tsc -p tsconfig.build.json" + "build": "tsc -p tsconfig.build.json", + "perf": "bun perf/perf.ts" }, "devDependencies": { "@biomejs/biome": "1.9.4", - "@types/node": "^24.8.1" + "@mitata/counters": "^0.0.8", + "@types/node": "^24.8.1", + "mitata": "^1.0.34" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/trimsock.js/packages/trimsock-js/perf/commands.perf.ts b/trimsock.js/packages/trimsock-js/perf/commands.perf.ts new file mode 100644 index 0000000..cbf4cf8 --- /dev/null +++ b/trimsock.js/packages/trimsock-js/perf/commands.perf.ts @@ -0,0 +1,34 @@ +import { Command, type CommandSpec } from "@lib/command.js"; +import { bench, do_not_optimize, run } from "mitata"; + +let idx = 0; +const commands: CommandSpec[] = [ + { name: "whereami", isRequest: true, requestId: "" }, + { + name: "session/set-game-id", + isRequest: true, + requestId: "", + params: ["RghrnyJK0mUA7cW05fCKo"], + }, + { + name: "lobby/create", + isRequest: true, + requestId: "", + params: ["noray://example.com:8890/Q9VKjXiAlwVK"], + kvParams: [["name", "Cool Lobby"]], + }, + { name: "lobby/lock", isRequest: true, requestId: "", text: "YuDqpQovXvpc" }, + { + name: "lobby/delete", + isRequest: true, + requestId: "", + params: ["YuDqpQovXvpc"], + }, +]; + +bench("serialize commands", () => { + do_not_optimize(Command.serialize(commands[idx])); + idx = (idx + 1) % commands.length; +}); + +await run(); diff --git a/trimsock.js/packages/trimsock-js/perf/perf.ts b/trimsock.js/packages/trimsock-js/perf/perf.ts new file mode 100644 index 0000000..ce1625b --- /dev/null +++ b/trimsock.js/packages/trimsock-js/perf/perf.ts @@ -0,0 +1,2 @@ +import "./commands.perf.ts"; +import "./reader.perf.ts"; diff --git a/trimsock.js/packages/trimsock-js/perf/reader.perf.ts b/trimsock.js/packages/trimsock-js/perf/reader.perf.ts new file mode 100644 index 0000000..a939d2b --- /dev/null +++ b/trimsock.js/packages/trimsock-js/perf/reader.perf.ts @@ -0,0 +1,20 @@ +import { TrimsockReader } from "@lib/reader.js"; +import { bench, do_not_optimize, run } from "mitata"; + +const reader = new TrimsockReader(); +let idx = 0; +const commands = [ + "whereami?\n", + "session/set-game-id RghrnyJK0mUA7cW05fCKo\n", + 'lobby/create? noray://example.com:8890/Q9VKjXiAlwVK name="Cool Lobby"\n', + "lobby/lock? YuDqpQovXvpc", + "lobby/delete? YuDqpQovXvpc", +]; + +bench("ingest and parse", () => { + reader.ingest(commands[idx]); + do_not_optimize([...reader.commands()]); + idx = (idx + 1) & commands.length; +}); + +await run(); diff --git a/trimsock.js/packages/trimsock-js/tsconfig.build.json b/trimsock.js/packages/trimsock-js/tsconfig.build.json index b98d28d..d084c31 100644 --- a/trimsock.js/packages/trimsock-js/tsconfig.build.json +++ b/trimsock.js/packages/trimsock-js/tsconfig.build.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", - "exclude": ["spec/*"], + "exclude": ["spec/*", "perf/*"], "compilerOptions": { "noEmit": false diff --git a/trimsock.js/packages/trimsock-js/tsconfig.json b/trimsock.js/packages/trimsock-js/tsconfig.json index 40de8c4..1a4a4e5 100644 --- a/trimsock.js/packages/trimsock-js/tsconfig.json +++ b/trimsock.js/packages/trimsock-js/tsconfig.json @@ -31,5 +31,5 @@ "outDir": "./dist/" }, - "include": ["lib/**/*.ts", "spec/**/*.ts", "./index.ts"] + "include": ["lib/**/*.ts", "spec/**/*.ts", "perf/**/*.ts", "./index.ts"] } diff --git a/trimsock.js/packages/trimsock-node/package.json b/trimsock.js/packages/trimsock-node/package.json index d4ce803..2688c8d 100644 --- a/trimsock.js/packages/trimsock-node/package.json +++ b/trimsock.js/packages/trimsock-node/package.json @@ -1,6 +1,6 @@ { "name": "@foxssake/trimsock-node", - "version": "0.18.1", + "version": "0.18.2", "module": "./dist/index.js", "type": "module", "author": "Tamás Gálffy",