Skip to content

Commit c8e05ff

Browse files
authored
Merge pull request #140 from lde-org/profiler
Add a profiler with flamegraph generation
2 parents 1284dc2 + ebdae4c commit c8e05ff

15 files changed

Lines changed: 813 additions & 57 deletions

File tree

CLAUDE.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ tests/ # Top-level integration test fixtures (e.g. some-package/)
2020
```
2121

2222
Each package has:
23+
2324
- `src/` — source files (or `src/init.lua` as the entry point)
2425
- `lde.json` — package manifest
2526
- `lde.lock` — lockfile (auto-generated, commit this)
@@ -64,6 +65,7 @@ Each package has:
6465
`lde install` / `lde run` populate `target/` with symlinks (or copies for packages with a build script). Each dep is installed at `target/<alias>`.
6566

6667
`package.path` is set to:
68+
6769
```
6870
target/?.lua
6971
target/?/init.lua
@@ -73,6 +75,7 @@ target/?.so (or .dll / .dylib)
7375
So `require("json")``target/json/init.lua` → symlink to `packages/json/src/init.lua`.
7476

7577
During `lde test`, `tests/` is also exposed as `target/tests`, so test files can do:
78+
7679
```lua
7780
local helper = require("tests.lib.something") -- resolves to tests/lib/something.lua
7881
```
@@ -88,6 +91,7 @@ local helper = require("tests.lib.something") -- resolves to tests/lib/somethin
8891
## Package API (`lde-core`)
8992

9093
`require("lde-core")` returns a table with:
94+
9195
- `lde.Package` — the Package class
9296
- `lde.Lockfile` — the Lockfile class
9397
- `lde.global` — global state/cache helpers
@@ -325,6 +329,19 @@ This outputs `packages/lde/lde` (or `lde.exe` on Windows). To install it globall
325329

326330
**Important:** Tests in `packages/lde/tests/` run the actual `lde` CLI binary via `env.execPath()`. If those tests fail after source changes, recompile and replace the binary first.
327331

332+
## Profiling `lde` code
333+
334+
You can profile an lde package with `lde run --profile` with `--flamegraph` to additionally generate a flamegraph.
335+
336+
To profile the entire test suite running this:
337+
338+
```sh
339+
cd packages/lde
340+
lde run --profile --flamegraph -- -C ../.. test
341+
```
342+
343+
This profiles the `lde` package entrypoint itself, not just an individual test file, and runs `lde test` as if it had been started from the repo root.
344+
328345
## Managing Dependencies
329346

330347
Always use `lde add` / `lde remove` instead of manually editing `lde.json`. Manual edits leave `lde.lock` out of sync and can break installs.
@@ -382,9 +399,9 @@ The project was previously named `lpm`. You may see `lpm.json` or `lpm-test` ref
382399

383400
**Windows:** The system GCC on `windows-latest` targets msvcrt, but lj-dist's `libluajit` is built against UCRT. This causes linker errors (`undefined reference to __imp_fseeko64` etc). CI downloads [llvm-mingw](https://github.com/mstorsjo/llvm-mingw) (a Clang/LLD MinGW-w64 toolchain targeting UCRT) and sets `SEA_CC`:
384401

385-
| Runner | `SEA_CC` |
386-
|---|---|
387-
| `windows-latest` | `x86_64-w64-mingw32-clang` |
402+
| Runner | `SEA_CC` |
403+
| ---------------- | --------------------------- |
404+
| `windows-latest` | `x86_64-w64-mingw32-clang` |
388405
| `windows-11-arm` | `aarch64-w64-mingw32-clang` |
389406

390-
**Android:** Android uses Bionic rather than glibc. The binary is compiled on the host using the Android NDK's clang (`aarch64-linux-android21-clang`), which links against Bionic. Tests run inside a Termux Docker container (`termux/termux-docker:aarch64` under QEMU on the ARM64 runner), which provides a matching Bionic environment.
407+
**Android:** Android uses Bionic rather than glibc. The binary is compiled on the host using the Android NDK's clang (`aarch64-linux-android21-clang`), which links against Bionic. Tests run inside a Termux Docker container (`termux/termux-docker:aarch64` under QEMU on the ARM64 runner), which provides a matching Bionic environment.

packages/clap/src/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function Args:option(desiredKey)
3333
else
3434
local key = string.sub(raw, 3)
3535

36-
if key == desiredKey and self.raw[i + 1] ~= nil then
36+
if key == desiredKey and self.raw[i + 1] ~= nil and self.raw[i + 1] ~= "--" then
3737
local _key = table.remove(self.raw, i)
3838
return table.remove(self.raw, i), i - 2
3939
elseif key == "" then

packages/clap/tests/main.test.lua

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
local test = require("lde-test")
2+
3+
local clap = require("clap")
4+
5+
test.it("option does not consume -- as a value", function()
6+
local args = clap.parse({ "--profile", "--flamegraph", "--", "--cwd", "../..", "test" })
7+
8+
test.truthy(args:flag("profile"))
9+
test.equal(args:option("flamegraph"), nil)
10+
test.equal(args:peek(), "--flamegraph")
11+
12+
local dash, dashPos = args:flag("")
13+
test.truthy(dash)
14+
test.equal(dashPos, 1)
15+
16+
local rest = args:drain(dashPos)
17+
test.equal(rest[1], "--flamegraph")
18+
test.equal(rest[2], "--cwd")
19+
test.equal(rest[3], "../..")
20+
test.equal(rest[4], "test")
21+
end)

packages/fs/tests/fs.test.lua

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,14 +425,13 @@ test.it("watch recursive detects creation in newly created subdirectory", functi
425425
local sub = path.join(d, "newdir")
426426
fs.mkdir(sub)
427427

428-
-- Drain the mkdir event and let the watcher register the new subdir
429-
local deadline = os.clock() + 1
430-
while os.clock() < deadline do w.poll() end
428+
-- Block on the mkdir event so the watcher can register the new subdir
429+
w.wait()
431430

432431
local before = #events
433432
fs.write(path.join(sub, "file.txt"), "hi")
434433

435-
deadline = os.clock() + 1
434+
local deadline = os.clock() + 1
436435
while #events == before and os.clock() < deadline do
437436
w.poll()
438437
end

packages/json/benchmarks/src/init.lua

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ end
4545

4646
-- ── fixtures ──────────────────────────────────────────────────────────────────
4747

48-
local SMALL = '{"name":"Alice","age":30,"active":true}'
48+
local SMALL = '{"name":"Alice","age":30,"active":true}'
4949

50-
local MEDIUM = json.encode({
50+
local MEDIUM = json.encode({
5151
users = (function()
5252
local t = {}
5353
for i = 1, 20 do
@@ -57,7 +57,7 @@ local MEDIUM = json.encode({
5757
end)()
5858
})
5959

60-
local LARGE = json.encode((function()
60+
local LARGE = json.encode((function()
6161
local t = {}
6262
for i = 1, 500 do
6363
t[i] = { id = i, name = "item" .. i, value = i * 3.14, tags = { "a", "b", "c" } }
@@ -77,41 +77,41 @@ local JSON5_SRC = [[{
7777
ports: [8080, 8443,],
7878
}]]
7979

80-
local SMALL_T = json.decode(SMALL)
81-
local MEDIUM_T = json.decode(MEDIUM)
82-
local LARGE_T = json.decode(LARGE)
83-
local JSON5_T = json.decode(JSON5_SRC)
80+
local SMALL_T = json.decode(SMALL)
81+
local MEDIUM_T = json.decode(MEDIUM)
82+
local LARGE_T = json.decode(LARGE)
83+
local JSON5_T = json.decode(JSON5_SRC)
8484

8585
-- ── run ───────────────────────────────────────────────────────────────────────
8686

8787
ansi.printf("\n{bold}json decode{reset}")
88-
bench("small object (~40 B)", function() json.decode(SMALL) end, 5000)
89-
bench("medium array (~20 objs)", function() json.decode(MEDIUM) end, 500)
90-
bench("large array (~500 objs)", function() json.decode(LARGE) end, 20)
91-
bench("json5 with comments", function() json.decode(JSON5_SRC) end, 2000)
88+
bench("small object (~40 B)", function() json.decode(SMALL) end, 5000)
89+
bench("medium array (~20 objs)", function() json.decode(MEDIUM) end, 500)
90+
bench("large array (~500 objs)", function() json.decode(LARGE) end, 20)
91+
bench("json5 with comments", function() json.decode(JSON5_SRC) end, 2000)
9292

9393
ansi.printf("\n{bold}json encode{reset}")
94-
bench("small object", function() json.encode(SMALL_T) end, 5000)
95-
bench("medium array", function() json.encode(MEDIUM_T) end, 500)
96-
bench("large array", function() json.encode(LARGE_T) end, 20)
97-
bench("json5 round-trip", function() json.encode(JSON5_T) end, 2000)
94+
bench("small object", function() json.encode(SMALL_T) end, 5000)
95+
bench("medium array", function() json.encode(MEDIUM_T) end, 500)
96+
bench("large array", function() json.encode(LARGE_T) end, 20)
97+
bench("json5 round-trip", function() json.encode(JSON5_T) end, 2000)
9898

9999
ansi.printf("\n{bold}json round-trip (decode + encode){reset}")
100-
bench("small", function() json.encode(json.decode(SMALL)) end, 5000)
100+
bench("small", function() json.encode(json.decode(SMALL)) end, 5000)
101101
bench("medium", function() json.encode(json.decode(MEDIUM)) end, 500)
102-
bench("large", function() json.encode(json.decode(LARGE)) end, 20)
102+
bench("large", function() json.encode(json.decode(LARGE)) end, 20)
103103

104104
ansi.printf("\n{bold}json decodeDocument only (zero-alloc){reset}")
105-
bench("small", function() json.decodeDocument(SMALL) end, 5000)
105+
bench("small", function() json.decodeDocument(SMALL) end, 5000)
106106
bench("medium", function() json.decodeDocument(MEDIUM) end, 500)
107-
bench("large", function() json.decode(LARGE) end, 20)
107+
bench("large", function() json.decodeDocument(LARGE) end, 20)
108108

109109
ansi.printf("\n{bold}cjson decode{reset}")
110-
bench("small object (~40 B)", function() cjson.decode(SMALL) end, 5000)
110+
bench("small object (~40 B)", function() cjson.decode(SMALL) end, 5000)
111111
bench("medium array (~20 objs)", function() cjson.decode(MEDIUM) end, 500)
112-
bench("large array (~500 objs)", function() cjson.decode(LARGE) end, 20)
112+
bench("large array (~500 objs)", function() cjson.decode(LARGE) end, 20)
113113

114114
ansi.printf("\n{bold}cjson encode{reset}")
115-
bench("small object", function() cjson.encode(cjson.decode(SMALL)) end, 5000)
116-
bench("medium array", function() cjson.encode(cjson.decode(MEDIUM)) end, 500)
117-
bench("large array", function() cjson.encode(cjson.decode(LARGE)) end, 20)
115+
bench("small object", function() cjson.encode(cjson.decode(SMALL)) end, 5000)
116+
bench("medium array", function() cjson.encode(cjson.decode(MEDIUM)) end, 500)
117+
bench("large array", function() cjson.encode(cjson.decode(LARGE)) end, 20)

packages/json/src/init.lua

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,6 @@ putValue = function(tape, v, indent, level, valueStyle)
339339
tape:put("Infinity")
340340
elseif v == -huge then
341341
tape:put("-Infinity")
342-
elseif v == floor(v) then
343-
tape:put(string.format("%d", v))
344342
else
345343
tape:put(tostring(v))
346344
end
@@ -784,17 +782,17 @@ local function materialise(doc, idx)
784782
end
785783
return arr
786784
elseif ty == TY_OBJECT then
787-
local obj = {}
788-
local keys = {}
785+
local obj = {}
786+
local keys = {}
789787
keyStore[obj] = keys
790788
local ki = tok.child
791789
while ki ~= 0 do
792-
src_s = doc.src; src_ptr = cast(u8p, src_s); src_len = #src_s
793-
local k = tokToString(doc.toks[ki])
794-
local vi = doc.toks[ki].next
795-
obj[k] = materialise(doc, vi)
790+
src_s = doc.src; src_ptr = cast(u8p, src_s); src_len = #src_s
791+
local k = tokToString(doc.toks[ki])
792+
local vi = doc.toks[ki].next
793+
obj[k] = materialise(doc, vi)
796794
keys[#keys + 1] = k
797-
ki = doc.toks[vi].next
795+
ki = doc.toks[vi].next
798796
end
799797
return obj
800798
end

0 commit comments

Comments
 (0)