use std::time::{Duration, Instant};
use crate::sizes::{repository_path, try_compile};
pub const SWIFT_BYTES_PER_LINE: usize = 800;
pub const SWIFT_NULL_PROGRAM_LINES: usize = 6;
pub const SWIFT_NULL_PROGRAM_JS: usize = 73_000;
pub const SWIFT_LARGEST_APP_LINES: usize = 1_094;
pub const SWIFT_LARGEST_APP_JS: usize = 1_210_000;
pub fn runtime_js_bytes() -> usize {
let release = |source| zdc_runtime::for_mode(source, zdc_runtime::Mode::Release).len();
release(zdc_runtime::SIGNAL_JS) + release(zdc_runtime::DOM_JS)
}
pub fn linked_runtime_bytes(runtime: &std::collections::BTreeSet<&'static str>) -> usize {
linked_runtime_bytes_in(runtime, zdc_codegen::Mode::Release)
}
pub fn linked_runtime_bytes_in(
runtime: &std::collections::BTreeSet<&'static str>,
mode: zdc_codegen::Mode,
) -> usize {
zdc_codegen::runtime_files(runtime, mode)
.iter()
.map(|(_, source)| source.len())
.sum()
}
pub fn code_lines(source: &str) -> usize {
source
.lines()
.filter(|line| {
let trimmed = line.trim_start();
!trimmed.is_empty() && !trimmed.starts_with('#')
})
.count()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Emitted {
pub name: String,
pub lines: usize,
pub code_lines: usize,
pub client_js: usize,
pub bundle: usize,
pub runtime_js: usize,
}
impl Emitted {
pub fn bytes_per_line(&self) -> usize {
self.client_js / self.code_lines.max(1)
}
pub fn bytes_per_line_with_runtime(&self) -> usize {
self.shipped() / self.code_lines.max(1)
}
pub fn shipped(&self) -> usize {
self.client_js + self.runtime_js
}
}
fn survey_sources() -> Vec<(String, String)> {
let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir(repository_path("examples"))
.expect("the examples directory exists")
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|ext| ext == "zd"))
.collect();
paths.sort();
paths.push(repository_path("crates/zdc-bench/bench/row.zd"));
let root = repository_path("");
paths
.into_iter()
.map(|path| {
let name = path
.strip_prefix(&root)
.unwrap_or(&path)
.to_string_lossy()
.into_owned();
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("reading {}: {e}", path.display()));
(name, source)
})
.collect()
}
pub fn survey() -> (Vec<Emitted>, Vec<(String, Vec<String>)>) {
let mut built = Vec::new();
let mut refused = Vec::new();
for (name, source) in survey_sources() {
match try_compile(&source, &name) {
Ok(bundle) => built.push(Emitted {
lines: source.lines().count(),
code_lines: code_lines(&source),
client_js: bundle.client_js.len(),
bundle: bundle.client_js.len()
+ bundle.styles_css.len()
+ bundle.index_html.as_deref().map_or(0, str::len)
+ bundle.manifest_json.len(),
runtime_js: linked_runtime_bytes(&bundle.runtime),
name,
}),
Err(errors) => refused.push((name, errors)),
}
}
(built, refused)
}
pub const NULL_PROGRAM: &str = "state greeting is client Text starting \"\"\n\
\n\
view\n\
\x20 Column\n\
\x20 Input greeting, hint is \"say something\"\n\
\x20 Text greeting\n";
pub const SMALLEST_PROGRAM: &str = "view\n\x20 Text \"x\"\n";
pub const FOREIGN_VIEW_PROGRAM: &str = "foreign gauge is client\n\
\x20 from \"./gauge.js\" as \"mount\"\n\
\x20 takes level is Whole\n\
\x20 gives view\n\
\n\
state level is client Whole starting 40\n\
\n\
view\n\
\x20 Column\n\
\x20 gauge level is level\n";
pub fn build(source: &str, name: &str) -> Emitted {
let bundle = try_compile(source, name)
.unwrap_or_else(|errors| panic!("{name} failed to compile:\n {}", errors.join("\n ")));
Emitted {
name: name.to_string(),
lines: source.lines().count(),
code_lines: code_lines(source),
client_js: bundle.client_js.len(),
bundle: bundle.client_js.len()
+ bundle.styles_css.len()
+ bundle.index_html.as_deref().map_or(0, str::len)
+ bundle.manifest_json.len(),
runtime_js: linked_runtime_bytes(&bundle.runtime),
}
}
pub fn program_with_signals(n: usize) -> String {
let mut source = String::new();
for i in 0..n {
source.push_str(&format!("state s{i} is client Whole starting {i}\n"));
}
source.push_str("\nview\n Column\n");
for i in 0..n {
source.push_str(&format!(" Text s{i}\n"));
}
source
}
pub fn program_with_components(depth: usize, count: usize, shared: bool) -> String {
let depth = depth.max(1);
let mut source = String::new();
if shared {
source.push_str("state caption is client Text starting \"caption\"\n\n");
}
source.push_str(
"component C0 with label\n \
Column\n \
Heading label\n \
Text \"a static caption line\"\n\n",
);
for level in 1..depth {
source.push_str(&format!(
"component C{level} with label\n \
Column\n \
C{} label\n \
Text \"a static caption line\"\n\n",
level - 1
));
}
source.push_str("view\n Column\n");
for i in 0..count {
let argument = if shared {
"caption".to_string()
} else {
format!("\"card {i}\"")
};
source.push_str(&format!(" C{} {argument}\n", depth - 1));
}
source
}
pub fn program_without_components(depth: usize, count: usize, shared: bool) -> String {
let depth = depth.max(1);
let mut source = String::new();
if shared {
source.push_str("state caption is client Text starting \"caption\"\n\n");
}
source.push_str("view\n Column\n");
for i in 0..count {
let argument = if shared {
"caption".to_string()
} else {
format!("\"card {i}\"")
};
write_inlined(depth, 8, &argument, &mut source);
}
source
}
fn write_inlined(remaining: usize, indent: usize, argument: &str, out: &mut String) {
let pad = " ".repeat(indent);
let inner = " ".repeat(indent + 4);
out.push_str(&format!("{pad}Column\n"));
if remaining == 1 {
out.push_str(&format!("{inner}Heading {argument}\n"));
} else {
write_inlined(remaining - 1, indent + 4, argument, out);
}
out.push_str(&format!("{inner}Text \"a static caption line\"\n"));
}
pub fn template_bytes(client_js: &str) -> usize {
let mut total = 0;
let mut rest = client_js;
while let Some(open) = rest.find("template('") {
rest = &rest[open + "template('".len()..];
let mut end = 0;
let bytes = rest.as_bytes();
while end < bytes.len() && !(bytes[end] == b'\'' && (end == 0 || bytes[end - 1] != b'\\')) {
end += 1;
}
total += end;
rest = &rest[end.min(bytes.len())..];
}
total
}
pub fn program_with_depth(n: usize) -> String {
let mut source = String::from("state leaf is client Text starting \"leaf\"\n\nview\n");
for i in 0..n {
source.push_str(&format!("{}Column\n", " ".repeat(4 * (i + 1))));
}
source.push_str(&format!("{}Text leaf\n", " ".repeat(4 * (n + 1))));
source
}
pub fn program_with_roots(defs: usize, roots: usize) -> String {
let defs = defs.max(1);
let mut source = String::from("function f0 with x\n give x + 1\n");
for i in 1..defs {
source.push_str(&format!(
"function f{i} with x\n give f{} with x\n",
i - 1
));
}
for i in 0..roots {
source.push_str(&format!(
"state v{i} is server Whole from f{} with {i}\n",
defs - 1
));
}
source.push_str("\nview\n Column\n");
for i in 0..roots {
source.push_str(&format!(" Text v{i}\n"));
}
source
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphTimes {
pub defs: usize,
pub roots: usize,
pub split: Duration,
pub ifc: Duration,
}
impl GraphTimes {
pub fn pairs(&self) -> usize {
self.defs * self.roots
}
}
pub fn time_graph_passes(source: &str, reps: u32) -> GraphTimes {
let reps = reps.max(1);
let program = zdc_parser::parse(source).unwrap_or_else(|e| panic!("{}", e.message));
let hir = zdc_resolve::Resolver::new(&program)
.resolve()
.unwrap_or_else(|errors| panic!("{}", errors[0].message));
let started = Instant::now();
for _ in 0..reps {
std::hint::black_box(zdc_graph::split(&hir));
}
let split = started.elapsed() / reps;
let tier_split = zdc_graph::split(&hir);
let started = Instant::now();
for _ in 0..reps {
std::hint::black_box(zdc_graph::ifc(&hir, &tier_split));
}
let ifc = started.elapsed() / reps;
GraphTimes {
defs: hir.defs.len(),
roots: tier_split.roots.len(),
split,
ifc,
}
}
pub fn deepest_fold() -> usize {
let folds = |n: usize| {
let mut context = boa_engine::Context::default();
let source = format!(
"function sumFrom(xs, i) {{ if (i >= xs.length) return 0; \
return xs[i] + sumFrom(xs, i + 1); }}\n\
sumFrom(new Array({n}).fill(1), 0)"
);
context
.eval(boa_engine::Source::from_bytes(source.as_bytes()))
.is_ok()
};
let mut deepest = 1usize;
let mut refused = 1usize << 14;
assert!(folds(deepest), "a one-element fold must succeed");
while deepest + 1 < refused {
let middle = deepest + (refused - deepest) / 2;
if folds(middle) {
deepest = middle;
} else {
refused = middle;
}
}
deepest
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn comments_and_blanks_are_not_program_lines() {
assert_eq!(code_lines("# a\n\n # b\nview\n Text \"x\"\n"), 2);
}
#[test]
fn the_null_program_is_six_lines_like_swifts() {
assert_eq!(NULL_PROGRAM.lines().count(), SWIFT_NULL_PROGRAM_LINES);
}
#[test]
fn the_generators_produce_the_sizes_they_claim() {
assert_eq!(code_lines(&program_with_signals(8)), 8 + 8 + 2);
let times = time_graph_passes(&program_with_roots(4, 4), 1);
assert_eq!(times.roots, 4 + 2);
}
}