You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/16 08:44:41 UTC
(maka) branch rust-preview updated: perf(runtime): avoid redundant startup schema copies
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/rust-preview by this push:
new 950685b431 perf(runtime): avoid redundant startup schema copies
950685b431 is described below
commit 950685b4310cc9ce2d4730ff73827de67dfb18d7
Author: Wang <m4...@gmail.com>
AuthorDate: Fri Oct 16 16:44:34 2026 +0800
perf(runtime): avoid redundant startup schema copies
Deserialize interface fields directly without buffering nested schemas in Serde Content. Borrow temporary plan imports from checked packages instead of cloning them.
Add a reproducible PTY startup comparison with isolated roots, interactive readiness checks, and failure evidence.
Generated-by: Codex
---
platform/plugin-core/src/contract.rs | 186 ++++++++++++++++++++++++++++++-
platform/plugin-core/src/plan.rs | 18 ++-
scripts/rust/README.md | 13 +++
scripts/rust/README.zh-CN.md | 9 ++
scripts/rust/benchmark-startup.py | 208 +++++++++++++++++++++++++++++++++++
5 files changed, 422 insertions(+), 12 deletions(-)
diff --git a/platform/plugin-core/src/contract.rs b/platform/plugin-core/src/contract.rs
index 89d598fa3b..d9bbce9e85 100644
--- a/platform/plugin-core/src/contract.rs
+++ b/platform/plugin-core/src/contract.rs
@@ -59,7 +59,7 @@ pub enum Transport {
Remote,
}
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Serialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum Interface {
Service {
@@ -79,6 +79,101 @@ pub enum Interface {
},
}
+// Deserialize the fields directly instead of buffering the entire tagged enum
+// as serde Content, which copies every nested schema before building its Value.
+impl<'de> Deserialize<'de> for Interface {
+ fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ #[derive(Deserialize)]
+ #[serde(rename_all = "snake_case")]
+ enum Kind {
+ Service,
+ Component,
+ Value,
+ Resource,
+ }
+
+ // A present JSON null must remain present for Value fields and must fail
+ // for typed fields. Ordinary Option deserialization would lose both rules.
+ fn present<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ T: Deserialize<'de>,
+ {
+ T::deserialize(deserializer).map(Some)
+ }
+
+ #[derive(Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct Fields {
+ kind: Kind,
+ #[serde(default, deserialize_with = "present")]
+ methods: Option<BTreeMap<String, Method>>,
+ #[serde(default, deserialize_with = "present")]
+ props: Option<Value>,
+ #[serde(default, deserialize_with = "present")]
+ events: Option<BTreeMap<String, Value>>,
+ #[serde(default, deserialize_with = "present")]
+ captures: Option<BTreeMap<String, Capture>>,
+ #[serde(default, deserialize_with = "present")]
+ schema: Option<Value>,
+ #[serde(default, deserialize_with = "present")]
+ resource: Option<String>,
+ }
+
+ struct FieldsVisitor;
+ impl<'de> serde::de::Visitor<'de> for FieldsVisitor {
+ type Value = Fields;
+
+ fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ formatter.write_str("an interface object")
+ }
+
+ fn visit_map<M: serde::de::MapAccess<'de>>(self, map: M) -> Result<Fields, M::Error> {
+ Fields::deserialize(serde::de::value::MapAccessDeserializer::new(map))
+ }
+ }
+ let fields = deserializer.deserialize_map(FieldsVisitor)?;
+ let allowed: &'static [&'static str] = match fields.kind {
+ Kind::Service => &["kind", "methods"],
+ Kind::Component => &["kind", "props", "events", "captures"],
+ Kind::Value => &["kind", "schema"],
+ Kind::Resource => &["kind", "resource", "schema"],
+ };
+ for (name, present) in [
+ ("methods", fields.methods.is_some()),
+ ("props", fields.props.is_some()),
+ ("events", fields.events.is_some()),
+ ("captures", fields.captures.is_some()),
+ ("schema", fields.schema.is_some()),
+ ("resource", fields.resource.is_some()),
+ ] {
+ if present && !allowed.contains(&name) {
+ return Err(serde::de::Error::unknown_field(name, allowed));
+ }
+ }
+ fn required<T, E: serde::de::Error>(value: Option<T>, name: &'static str) -> Result<T, E> {
+ value.ok_or_else(|| E::missing_field(name))
+ }
+ Ok(match fields.kind {
+ Kind::Service => Self::Service {
+ methods: required(fields.methods, "methods")?,
+ },
+ Kind::Component => Self::Component {
+ props: required(fields.props, "props")?,
+ events: required(fields.events, "events")?,
+ captures: required(fields.captures, "captures")?,
+ },
+ Kind::Value => Self::Value {
+ schema: required(fields.schema, "schema")?,
+ },
+ Kind::Resource => Self::Resource {
+ resource: required(fields.resource, "resource")?,
+ schema: required(fields.schema, "schema")?,
+ },
+ })
+ }
+}
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Method {
@@ -265,3 +360,92 @@ impl Cardinality {
}
}
}
+
+#[cfg(test)]
+mod interface_deserialization_tests {
+ use super::Interface;
+ use serde_json::{Value, json};
+
+ #[test]
+ fn interfaces_round_trip_with_the_tag_before_or_after_fields() {
+ for value in [
+ json!({"kind": "service", "methods": {"read": {
+ "mode": "query", "input": {"type": "object"},
+ "output": {"type": "string"}, "resources": {}
+ }}}),
+ json!({"kind": "component", "props": {}, "events": {"change": true}, "captures": {}}),
+ json!({"kind": "value", "schema": true}),
+ json!({"kind": "resource", "resource": "text", "schema": {}}),
+ ] {
+ let fields = value.as_object().unwrap();
+ let tag = serde_json::to_string(&fields["kind"]).unwrap();
+ let rest = fields
+ .iter()
+ .filter(|(name, _)| *name != "kind")
+ .map(|(name, value)| format!("{name:?}:{value}"))
+ .collect::<Vec<_>>()
+ .join(",");
+ for text in [
+ format!("{{\"kind\":{tag},{rest}}}"),
+ format!("{{{rest},\"kind\":{tag}}}"),
+ ] {
+ let interface: Interface = serde_json::from_str(&text).unwrap();
+ assert_eq!(serde_json::to_value(&interface).unwrap(), value);
+ assert_eq!(
+ serde_json::from_value::<Interface>(value.clone()).unwrap(),
+ interface
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn null_schema_values_are_present_but_typed_fields_require_their_types() {
+ for text in [
+ r#"{"kind":"value","schema":null}"#,
+ r#"{"kind":"resource","resource":"text","schema":null}"#,
+ r#"{"kind":"component","props":null,"events":{},"captures":{}}"#,
+ ] {
+ let interface: Interface = serde_json::from_str(text).unwrap();
+ assert_eq!(
+ serde_json::to_value(interface).unwrap(),
+ serde_json::from_str::<Value>(text).unwrap()
+ );
+ }
+ for text in [
+ r#"{"kind":"service","methods":null}"#,
+ r#"{"kind":"component","props":{},"events":null,"captures":{}}"#,
+ r#"{"kind":"component","props":{},"events":{},"captures":null}"#,
+ r#"{"kind":"resource","resource":null,"schema":{}}"#,
+ r#"{"kind":null,"schema":{}}"#,
+ ] {
+ assert!(serde_json::from_str::<Interface>(text).is_err(), "{text}");
+ }
+ }
+
+ #[test]
+ fn malformed_or_cross_variant_fields_are_rejected() {
+ for text in [
+ r#"{"kind":"service"}"#,
+ r#"{"kind":"component","props":{},"events":{}}"#,
+ r#"{"kind":"component","props":{},"captures":{}}"#,
+ r#"{"kind":"component","events":{},"captures":{}}"#,
+ r#"{"kind":"value"}"#,
+ r#"{"kind":"resource","schema":{}}"#,
+ r#"{"kind":"resource","resource":"text"}"#,
+ r#"{"schema":{}}"#,
+ r#"{"kind":"unknown","schema":{}}"#,
+ r#"{"kind":"value","schema":{},"extra":true}"#,
+ r#"{"kind":"value","kind":"value","schema":{}}"#,
+ r#"{"kind":"value","schema":null,"schema":{}}"#,
+ r#"{"kind":"component","props":null,"props":{},"events":{},"captures":{}}"#,
+ r#"{"kind":"service","methods":{},"schema":null}"#,
+ r#"{"kind":"component","props":{},"events":{},"captures":{},"methods":{}}"#,
+ r#"{"kind":"value","schema":{},"resource":"text"}"#,
+ r#"{"kind":"resource","resource":"text","schema":{},"events":{}}"#,
+ r#"["value",null,null,null,null,{},null]"#,
+ ] {
+ assert!(serde_json::from_str::<Interface>(text).is_err(), "{text}");
+ }
+ }
+}
diff --git a/platform/plugin-core/src/plan.rs b/platform/plugin-core/src/plan.rs
index 4027b9a02b..89c3afc41b 100644
--- a/platform/plugin-core/src/plan.rs
+++ b/platform/plugin-core/src/plan.rs
@@ -193,21 +193,21 @@ enum PendingExport {
Forward(Target),
}
-struct Pending {
+struct Pending<'a> {
instance: Instance,
node: Node,
- imports: BTreeMap<String, Import>,
+ imports: &'a BTreeMap<String, Import>,
exports: BTreeMap<String, PendingExport>,
}
struct Builder<'a> {
packages: &'a Packages,
target: EntryTarget,
- nodes: BTreeMap<InstanceId, Pending>,
+ nodes: BTreeMap<InstanceId, Pending<'a>>,
aliases: BTreeMap<Vec<String>, InstanceId>,
}
-impl Builder<'_> {
+impl<'a> Builder<'a> {
fn expand(
&mut self,
nodes: &BTreeMap<String, Node>,
@@ -342,11 +342,7 @@ impl Builder<'_> {
)
})
.collect();
- (
- Some(Arc::new(entry.clone())),
- entry.imports.clone(),
- exports,
- )
+ (Some(Arc::new(entry.clone())), &entry.imports, exports)
}
PackageUse::Composition => {
if configuration != serde_json::json!({}) {
@@ -357,7 +353,7 @@ impl Builder<'_> {
})?;
(
None,
- template.imports.clone(),
+ &template.imports,
template
.exports
.iter()
@@ -430,7 +426,7 @@ enum Resolving {
}
struct Resolver<'a> {
- nodes: &'a BTreeMap<InstanceId, Pending>,
+ nodes: &'a BTreeMap<InstanceId, Pending<'a>>,
aliases: &'a BTreeMap<Vec<String>, InstanceId>,
remote: &'a BTreeMap<RemoteCapability, Arc<Export>>,
}
diff --git a/scripts/rust/README.md b/scripts/rust/README.md
index c8c4d339b0..3145c97efa 100644
--- a/scripts/rust/README.md
+++ b/scripts/rust/README.md
@@ -39,3 +39,16 @@ node scripts/rust/notices.mjs check
```
Review the generated inventory and license texts. [notices-sources.json](notices-sources.json) records reviewed upstream exceptions. Release assembly also includes notices from the actual V8 build and Rust standard library. The TSV inventory does not replace full license texts.
+
+To compare direct TUI startup on macOS/Linux, build and retain each executable,
+then run:
+
+```sh
+python3 scripts/rust/benchmark-startup.py --binary target/debug/maka \
+ --baseline /tmp/maka-before --runs 5 --output /tmp/maka-startup
+```
+
+The benchmark uses isolated roots, measures installation separately, and alternates
+reopens. It waits for the home screen and verifies the command palette before
+quitting. The output directory retains raw terminal output, failures, and timing
+medians/ranges. Keep build profiles equal and run without competing builds.
diff --git a/scripts/rust/README.zh-CN.md b/scripts/rust/README.zh-CN.md
index 23c1a5afd1..0669135346 100644
--- a/scripts/rust/README.zh-CN.md
+++ b/scripts/rust/README.zh-CN.md
@@ -39,3 +39,12 @@ node scripts/rust/notices.mjs check
```
检查生成的清单和许可证文本。[notices-sources.json](notices-sources.json)记录已审查的上游例外。发布组装还包含实际 V8 构建与 Rust 标准库的 notices。TSV 清单不能替代完整许可证文本。
+
+在 macOS/Linux 上比较直接启动 TUI 的耗时:先构建并保留前后两个可执行文件,再运行:
+
+```sh
+python3 scripts/rust/benchmark-startup.py --binary target/debug/maka \
+ --baseline /tmp/maka-before --runs 5 --output /tmp/maka-startup
+```
+
+基准使用独立数据目录,首次安装单独计时,重开按前后版本交替执行。它等待首页出现并确认命令面板可用后退出。输出目录保留原始终端输出、失败次数及耗时中位数和范围。两版应使用相同构建配置,测量期间避免同时构建。
diff --git a/scripts/rust/benchmark-startup.py b/scripts/rust/benchmark-startup.py
new file mode 100644
index 0000000000..6548ba2373
--- /dev/null
+++ b/scripts/rust/benchmark-startup.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Measure real TUI startup in a 120x40 PTY (macOS/Linux, standard library).
+
+Example: benchmark-startup.py --binary target/debug/maka --baseline /tmp/maka-old \
+ --runs 5 --output /tmp/maka-startup-evidence
+Installation is measured separately; reopen rounds alternate baseline/candidate.
+Readiness requires Settings + New session, then Ctrl-K showing Close commands.
+Evidence retains isolated installation roots, raw ANSI, and results.json. No sampling.
+"""
+import argparse
+import errno
+import fcntl
+import json
+import os
+from pathlib import Path
+import pty
+import re
+import select
+import signal
+import statistics
+import struct
+import subprocess
+import tempfile
+import termios
+import time
+
+ANSI = re.compile(rb'\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07]*?(?:\x07|\x1b\\)')
+QUERIES = [(b'\x1b[6n', b'\x1b[1;1R'), (b'\x1b[c', b'\x1b[?1;2c'),
+ (b'\x1b[>c', b'\x1b[>0;0;0c'),
+ (b'\x1b]11;?', b'\x1b]11;rgb:1010/1010/1010\x1b\\')]
+
+
+def terminal_owner():
+ os.setsid()
+ fcntl.ioctl(0, termios.TIOCSCTTY, 0)
+
+
+def stop(process):
+ for sig in (signal.SIGTERM, signal.SIGKILL):
+ try:
+ os.killpg(process.pid, sig)
+ except ProcessLookupError:
+ break
+ try:
+ process.wait(timeout=3)
+ break
+ except subprocess.TimeoutExpired:
+ continue
+ process.wait()
+
+
+def cleanup(case):
+ # Public Hosts may detach into a new session. Only target our unique roots.
+ roots = {str(case / 'host'), str(case / 'ui')}
+ listing = subprocess.run(['ps', '-axo', 'pid=,command='], capture_output=True, text=True)
+ for line in listing.stdout.splitlines():
+ try:
+ pid, command = line.strip().split(None, 1)
+ # ps renders argv without shell quoting, including paths with spaces.
+ owns_root = any(re.search(r'(?<!\S)--root ' + re.escape(root) + r'(?: |$)', command) for root in roots)
+ if owns_root and int(pid) != os.getpid():
+ os.kill(int(pid), signal.SIGTERM)
+ except (ValueError, OSError):
+ continue
+
+
+def measure(binary, case, name, installation, timeout):
+ master, slave = pty.openpty()
+ fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 40, 120, 0, 0))
+ args = [str(binary), 'application', 'launch', '--root', str(case / 'ui')]
+ if installation:
+ args += ['--configuration', str(case / 'installation.json')]
+ env = dict(os.environ, TERM='xterm-256color', COLORTERM='truecolor', LC_ALL='en_US.UTF-8')
+ env.pop('NO_COLOR', None)
+ output, queries = bytearray(), set()
+ result = dict(run=name, first_install=installation, ready_s=None, interactive_s=None,
+ exit_code=None, error=None, raw_ansi=str(case / f'{name}.ansi'))
+ process = None
+ started = time.monotonic()
+ try:
+ process = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave,
+ cwd=case, env=env, preexec_fn=terminal_owner)
+ os.close(slave)
+ slave = None
+ while time.monotonic() - started < timeout:
+ readable, _, _ = select.select([master], [], [], 0.05)
+ if readable:
+ try:
+ chunk = os.read(master, 65536)
+ except OSError as error:
+ if error.errno == errno.EIO:
+ break
+ raise
+ if not chunk:
+ break
+ output.extend(chunk)
+ for pattern, reply in QUERIES:
+ for match in re.finditer(re.escape(pattern), output):
+ key = (pattern, match.start())
+ if key not in queries:
+ os.write(master, reply)
+ queries.add(key)
+ clean = ANSI.sub(b'', output)
+ if result['ready_s'] is None and b'Settings' in clean and b'New session' in clean:
+ result['ready_s'] = time.monotonic() - started
+ os.write(master, b'\x0b')
+ if result['ready_s'] is not None and result['interactive_s'] is None and b'Close commands' in clean:
+ result['interactive_s'] = time.monotonic() - started
+ os.write(master, b'\x1b\x11')
+ if process.poll() is not None and not readable:
+ break
+ if process.poll() is None:
+ process.wait(timeout=max(0.01, timeout - (time.monotonic() - started)))
+ if result['interactive_s'] is None:
+ result['error'] = 'exited before command palette became usable'
+ elif process.returncode != 0:
+ result['error'] = 'nonzero exit after Ctrl-Q'
+ except (OSError, subprocess.TimeoutExpired) as error:
+ result['error'] = f'launch/exit deadline exceeded ({timeout}s)' if isinstance(error, subprocess.TimeoutExpired) else f'{type(error).__name__}: {error}'
+ finally:
+ if slave is not None:
+ os.close(slave)
+ if process is not None:
+ if process.poll() is None:
+ stop(process)
+ result['exit_code'] = process.returncode
+ os.close(master)
+ Path(result['raw_ansi']).write_bytes(output)
+ result['total_s'] = time.monotonic() - started
+ if result['error'] is not None:
+ cleanup(case)
+ print(json.dumps(result), flush=True)
+ return result
+
+
+def summarize(rows):
+ successful = [row for row in rows if row['error'] is None]
+ result = dict(runs=len(rows), failures=len(rows) - len(successful))
+ for field in ('ready_s', 'interactive_s'):
+ values = [row[field] for row in successful]
+ result[field] = dict(median=statistics.median(values), min=min(values), max=max(values)) if values else None
+ return result
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument('--binary', required=True, type=Path, help='candidate CLI executable')
+ parser.add_argument('--baseline', type=Path, help='optional baseline CLI executable')
+ parser.add_argument('--runs', type=int, default=5, help='reopen runs per binary (default: 5)')
+ parser.add_argument('--output', required=True, type=Path, help='evidence directory; roots are always new')
+ parser.add_argument('--timeout', type=float, default=60, help='seconds allowed per launch including exit')
+ options = parser.parse_args()
+ if options.runs < 1 or options.timeout <= 0:
+ parser.error('--runs and --timeout must be positive')
+ binaries = {'baseline': options.baseline, 'candidate': options.binary}
+ binaries = {label: path.resolve() for label, path in binaries.items() if path is not None}
+ for path in binaries.values():
+ if not path.is_file() or not os.access(path, os.X_OK):
+ parser.error(f'not an executable: {path}')
+ options.output.mkdir(parents=True, exist_ok=True)
+ evidence = Path(tempfile.mkdtemp(prefix='startup-', dir=options.output.resolve()))
+ cases, results = {}, dict(binaries={key: str(path) for key, path in binaries.items()},
+ terminal={'columns': 120, 'rows': 40}, installation={}, reopens={}, summary={})
+ try:
+ for label, binary in binaries.items():
+ case = evidence / label
+ case.mkdir()
+ (case / 'workspace').mkdir()
+ cases[label] = case
+ configuration = dict(presentation='tui', locale='en', scheme='dark', hostRoot=str(case / 'host'),
+ agent={'workspaceRoots': [{'label': 'Workspace', 'path': str(case / 'workspace')}],
+ 'tools': [], 'permissions': {'access': 'read_only', 'requestPermissions': True}},
+ skills={'bundled': True, 'sources': []})
+ (case / 'installation.json').write_text(json.dumps(configuration))
+ results['installation'][label] = measure(binary, case, 'installation', True, options.timeout)
+ results['reopens'][label] = []
+ for run in range(options.runs):
+ for label, binary in binaries.items():
+ if results['installation'][label]['error'] is None:
+ results['reopens'][label].append(measure(binary, cases[label], f'reopen-{run + 1}', False, options.timeout))
+ finally:
+ for label, case in cases.items():
+ cleanup(case)
+ results['summary'][label] = summarize(results['reopens'].get(label, []))
+ results['failures'] = sum(bool(row['error']) for row in results['installation'].values()) + sum(item['failures'] for item in results['summary'].values())
+ (evidence / 'results.json').write_text(json.dumps(results, indent=2) + '\n')
+ print(f'Evidence: {evidence / "results.json"}', flush=True)
+ return int(results['failures'] > 0)
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org