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