Skip to content

Commit 6684b05

Browse files
committed
Merge pull request 'New features, improvements and fixes' (#18) from feat/polish-first into main
Reviewed-on: https://gitea.renux.dev/renux/project-actions/pulls/18
2 parents 0babdae + 1b8bcab commit 6684b05

43 files changed

Lines changed: 2416 additions & 5344 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ node_modules/
33
*.vsix
44
!releases/*.vsix
55
.vscode-test/
6+
package-lock.json
67
.claude/
78
.agents/
89
docs/

.vscodeignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ node_modules/**
1313
.agents/**
1414
.claude/**
1515
.opencode/**
16+
benchmarks/**
17+
scripts/**

README.md

Lines changed: 128 additions & 96 deletions
Large diffs are not rendered by default.

benchmarks/detectors.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import * as fs from "fs";
2+
import * as os from "os";
3+
import * as path from "path";
4+
import { benchmark } from "./lib/timing";
5+
import { printTable } from "./lib/format";
6+
import { detectors } from "../src/detectors/index";
7+
8+
function createFixtures(root: string) {
9+
fs.mkdirSync(root, { recursive: true });
10+
11+
const pkgScripts: Record<string, string> = {};
12+
for (let i = 1; i <= 20; i++) {
13+
pkgScripts[`script-${i}`] = `echo ${i}`;
14+
}
15+
fs.writeFileSync(
16+
path.join(root, "package.json"),
17+
JSON.stringify({ name: "test", scripts: pkgScripts }, null, 2),
18+
);
19+
fs.writeFileSync(path.join(root, "bun.lock"), "");
20+
21+
const composerScripts: Record<string, string> = {};
22+
for (let i = 1; i <= 10; i++) {
23+
composerScripts[`script-${i}`] = `echo ${i}`;
24+
}
25+
fs.writeFileSync(
26+
path.join(root, "composer.json"),
27+
JSON.stringify({ scripts: composerScripts }, null, 2),
28+
);
29+
30+
const makeTargets: string[] = [];
31+
for (let i = 1; i <= 50; i++) {
32+
makeTargets.push(`target-${i}`);
33+
}
34+
const makefileLines: string[] = [
35+
`.PHONY: ${makeTargets.join(" ")}`,
36+
...makeTargets.map((t) => `${t}:\n\techo ${t}`),
37+
];
38+
fs.writeFileSync(path.join(root, "Makefile"), makefileLines.join("\n"));
39+
40+
const rakeTasks: string[] = [];
41+
for (let i = 1; i <= 15; i++) {
42+
rakeTasks.push(`task :task-${i}`);
43+
}
44+
fs.writeFileSync(path.join(root, "Rakefile"), rakeTasks.join("\n"));
45+
46+
const profiles: string[] = [];
47+
for (let i = 1; i <= 5; i++) {
48+
profiles.push(`<profile><id>profile-${i}</id></profile>`);
49+
}
50+
fs.writeFileSync(
51+
path.join(root, "pom.xml"),
52+
`<project>\n <profiles>\n ${profiles.join("\n ")}\n </profiles>\n</project>`,
53+
);
54+
55+
const gradleTasks: string[] = [];
56+
for (let i = 1; i <= 10; i++) {
57+
const q = i % 3 === 0 ? '"' : i % 3 === 1 ? "'" : "";
58+
gradleTasks.push(`task ${q}custom-${i}${q}`);
59+
}
60+
fs.writeFileSync(path.join(root, "build.gradle"), gradleTasks.join("\n"));
61+
62+
const cargoLines: string[] = [
63+
"[package]",
64+
'name = "pkg"',
65+
'version = "0.1.0"',
66+
"",
67+
"[[bin]]",
68+
'name = "bin1"',
69+
"",
70+
"[[bin]]",
71+
'name = "bin2"',
72+
"",
73+
"[[example]]",
74+
'name = "ex1"',
75+
"",
76+
"[[example]]",
77+
'name = "ex2"',
78+
"",
79+
"[[test]]",
80+
'name = "t1"',
81+
"",
82+
"[[test]]",
83+
'name = "t2"',
84+
"",
85+
"[[bench]]",
86+
'name = "b1"',
87+
"",
88+
"[workspace]",
89+
'members = ["member-a", "member-b", "member-c"]',
90+
];
91+
fs.writeFileSync(path.join(root, "Cargo.toml"), cargoLines.join("\n"));
92+
93+
fs.writeFileSync(path.join(root, "go.mod"), "module example.com/test\ngo 1.21");
94+
for (const name of ["server", "worker", "cli"]) {
95+
const dir = path.join(root, "cmd", name);
96+
fs.mkdirSync(dir, { recursive: true });
97+
fs.writeFileSync(path.join(dir, "main.go"), "package main\n");
98+
}
99+
100+
const pyprojectLines: string[] = [
101+
"[build-system]",
102+
'requires = ["setuptools"]',
103+
"",
104+
"[tool.pytest.ini_options]",
105+
'minversion = "7.0"',
106+
"",
107+
"[tool.tox]",
108+
'env_list = ["py311"]',
109+
"",
110+
"[project.scripts]",
111+
's1 = "m1:main"',
112+
's2 = "m2:main"',
113+
's3 = "m3:main"',
114+
's4 = "m4:main"',
115+
's5 = "m5:main"',
116+
];
117+
fs.writeFileSync(path.join(root, "pyproject.toml"), pyprojectLines.join("\n"));
118+
}
119+
120+
async function main() {
121+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "benchmark-detectors-"));
122+
createFixtures(root);
123+
124+
const rows: {
125+
name: string;
126+
min: number;
127+
max: number;
128+
mean: number;
129+
p50: number;
130+
p95: number;
131+
p99: number;
132+
}[] = [];
133+
for (const detector of detectors) {
134+
const stats = await benchmark(() => detector.detect(root), { warmup: 5, iterations: 100 });
135+
rows.push({ name: detector.id, ...stats });
136+
}
137+
138+
const parallelStats = await benchmark(() => Promise.all(detectors.map((d) => d.detect(root))), {
139+
warmup: 5,
140+
iterations: 100,
141+
});
142+
rows.push({ name: "all-parallel", ...parallelStats });
143+
144+
printTable(
145+
"Detector Performance (100 runs each, in µs)",
146+
["name", "min", "max", "mean", "p50", "p95", "p99"],
147+
rows,
148+
);
149+
150+
fs.rmSync(root, { recursive: true });
151+
}
152+
153+
main().catch((err) => {
154+
console.error(err);
155+
process.exit(1);
156+
});

benchmarks/lib/format.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export function formatSize(bytes: number): string {
2+
if (bytes === 0) {
3+
return "0.0 KB";
4+
}
5+
const kb = bytes / 1024;
6+
if (kb < 1024) {
7+
return `${kb.toFixed(1)} KB`;
8+
}
9+
const mb = kb / 1024;
10+
if (mb < 1024) {
11+
return `${mb.toFixed(1)} MB`;
12+
}
13+
return `${(mb / 1024).toFixed(1)} GB`;
14+
}
15+
16+
export function printTable(
17+
title: string,
18+
headers: string[],
19+
rows: Record<string, string | number>[],
20+
): void {
21+
const colWidths = headers.map((h) => {
22+
const headerLen = h.length;
23+
const maxValLen = Math.max(...rows.map((r) => String(r[h]).length));
24+
return Math.max(headerLen, maxValLen);
25+
});
26+
27+
const totalWidth = colWidths.reduce((a, b) => a + b, 0) + (colWidths.length - 1) * 2;
28+
29+
console.log(title);
30+
console.log("─".repeat(totalWidth));
31+
console.log(
32+
headers.map((h, i) => (i === 0 ? h.padEnd(colWidths[i]) : h.padStart(colWidths[i]))).join(" "),
33+
);
34+
for (const row of rows) {
35+
const cells = headers.map((h, i) => {
36+
const val = String(row[h]);
37+
return i === 0 ? val.padEnd(colWidths[i]) : val.padStart(colWidths[i]);
38+
});
39+
console.log(cells.join(" "));
40+
}
41+
console.log("─".repeat(totalWidth));
42+
}

benchmarks/lib/timing.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export interface Stats {
2+
min: number;
3+
max: number;
4+
mean: number;
5+
p50: number;
6+
p95: number;
7+
p99: number;
8+
}
9+
10+
export function computeStats(timings: number[]): Stats {
11+
const sorted = [...timings].sort((a, b) => a - b);
12+
const min = sorted[0];
13+
const max = sorted[sorted.length - 1];
14+
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
15+
const percentile = (p: number) => sorted[Math.ceil(p * sorted.length) - 1];
16+
return {
17+
min: Math.round(min),
18+
max: Math.round(max),
19+
mean: Math.round(mean),
20+
p50: Math.round(percentile(0.5)),
21+
p95: Math.round(percentile(0.95)),
22+
p99: Math.round(percentile(0.99)),
23+
};
24+
}
25+
26+
export async function benchmark(
27+
fn: () => void | Promise<void>,
28+
options: { warmup: number; iterations: number },
29+
): Promise<Stats> {
30+
for (let i = 0; i < options.warmup; i++) {
31+
await fn();
32+
}
33+
const timings: number[] = [];
34+
for (let i = 0; i < options.iterations; i++) {
35+
const start = process.hrtime.bigint();
36+
await fn();
37+
const elapsed = Number(process.hrtime.bigint() - start) / 1000;
38+
timings.push(elapsed);
39+
}
40+
return computeStats(timings);
41+
}

benchmarks/size.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import { formatSize } from "./lib/format";
4+
5+
const root = process.cwd();
6+
7+
function getFileSize(filePath: string): number {
8+
try {
9+
return fs.statSync(filePath).size;
10+
} catch {
11+
return 0;
12+
}
13+
}
14+
15+
function getDirSize(dirPath: string): number {
16+
let total = 0;
17+
try {
18+
const entries = fs.readdirSync(dirPath, { recursive: true }) as string[];
19+
for (const entry of entries) {
20+
const fullPath = path.join(dirPath, entry);
21+
const stat = fs.statSync(fullPath);
22+
if (stat.isFile()) {
23+
total += stat.size;
24+
}
25+
}
26+
} catch {
27+
return 0;
28+
}
29+
return total;
30+
}
31+
32+
function getVsixFiles(): { name: string; size: number }[] {
33+
try {
34+
const entries = fs.readdirSync(root, { withFileTypes: true });
35+
return entries
36+
.filter((e) => e.isFile() && e.name.endsWith(".vsix"))
37+
.map((e) => ({ name: e.name, size: getFileSize(path.join(root, e.name)) }));
38+
} catch {
39+
return [];
40+
}
41+
}
42+
43+
const extensionJsSize = getFileSize(path.join(root, "out", "extension.js"));
44+
const vsixFiles = getVsixFiles();
45+
const outDirSize = getDirSize(path.join(root, "out"));
46+
const nodeModulesSize = getDirSize(path.join(root, "node_modules"));
47+
48+
const rows: { label: string; size: number; note?: string }[] = [
49+
{ label: "out/extension.js", size: extensionJsSize },
50+
];
51+
52+
if (vsixFiles.length === 0) {
53+
rows.push({ label: "VSIX", size: 0, note: "none" });
54+
} else {
55+
for (const v of vsixFiles) {
56+
rows.push({ label: "VSIX", size: v.size, note: v.name });
57+
}
58+
}
59+
60+
rows.push({ label: "out/ total", size: outDirSize });
61+
rows.push({ label: "node_modules/", size: nodeModulesSize });
62+
63+
const maxLabel = Math.max(...rows.map((r) => r.label.length));
64+
const divider = "─".repeat(44);
65+
66+
console.log("Bundle Size Report");
67+
console.log(divider);
68+
69+
for (const row of rows) {
70+
let line = row.label.padEnd(maxLabel + 4) + formatSize(row.size);
71+
if (row.note) {
72+
line += ` (${row.note})`;
73+
}
74+
console.log(line);
75+
}

0 commit comments

Comments
 (0)