aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--premake5.lua2
-rw-r--r--test/.gitignore7
-rw-r--r--test/Makefile77
-rw-r--r--test/hello.cpp16
-rw-r--r--test/instr_demo.c51
-rw-r--r--test/macro_demo.c47
-rw-r--r--test/memory_demo.cpp31
-rw-r--r--test/sample_demo.cpp47
-rw-r--r--test/trace_demo.cpp49
10 files changed, 328 insertions, 4 deletions
diff --git a/.gitignore b/.gitignore
index 98b8b6b..4af3331 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
bin/
bin-int/
-Makefile
+/Makefile
*.make
*.sln
@@ -17,12 +17,9 @@ compile_commands.json
compile_flags.txt
.clangd
-# imgui layout state
imgui.ini
-# llvm pulled down by `premake5 fetch-llvm`
ext/llvm/
ext/llvm-staging/
ext/*.tar.xz
-test/
diff --git a/premake5.lua b/premake5.lua
index efae3cd..e0dc372 100644
--- a/premake5.lua
+++ b/premake5.lua
@@ -281,6 +281,8 @@ newaction {
os.rmdir("bin-int")
os.rmdir(".vs")
os.remove("Makefile")
+ os.remove("imgui.ini")
+ os.remove("compile_commands.json")
local patterns = {
"*.make",
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 0000000..d15c2a1
--- /dev/null
+++ b/test/.gitignore
@@ -0,0 +1,7 @@
+*
+!.gitignore
+!Makefile
+!*.c
+!*.cpp
+!*.h
+!*.hpp
diff --git a/test/Makefile b/test/Makefile
new file mode 100644
index 0000000..d8401b5
--- /dev/null
+++ b/test/Makefile
@@ -0,0 +1,77 @@
+# hsdbg test workspace
+#
+# Targets:
+# hello a trivial program, for a first launch and single-stepping
+# macro_demo layered #define macros, for the macro unwrapper
+# trace_demo nested calls, for the flame-chart timeline
+# sample_demo plain busywork, for the sampling profiler
+# memory_demo allocation waves, for the resident-memory graph
+# instr_demo -finstrument-functions + the trace runtime, auto-traced
+
+UNAME := $(shell uname)
+
+CC ?= clang
+CXX ?= clang++
+
+CFLAGS := -g -O0 -Wall -Wextra
+CXXFLAGS := -g -O0 -Wall -Wextra -std=c++23
+
+OBJ := .obj
+TRACE_RUNTIME := ../ext/hsdbg_trace/hsdbg_trace.c
+
+TARGETS := hello macro_demo trace_demo sample_demo memory_demo instr_demo
+
+ifeq ($(UNAME),Darwin)
+ DSYMUTIL := dsymutil
+else
+ DSYMUTIL := true
+endif
+
+all: $(TARGETS)
+
+$(OBJ):
+ mkdir -p $(OBJ)
+
+$(OBJ)/%.o: %.cpp | $(OBJ)
+ $(CXX) $(CXXFLAGS) -c $< -o $@
+
+$(OBJ)/%.o: %.c | $(OBJ)
+ $(CC) $(CFLAGS) -c $< -o $@
+
+hello: $(OBJ)/hello.o
+ $(CXX) $(CXXFLAGS) $^ -o $@
+ $(DSYMUTIL) $@
+
+macro_demo: $(OBJ)/macro_demo.o
+ $(CC) $(CFLAGS) $^ -o $@
+ $(DSYMUTIL) $@
+
+trace_demo: $(OBJ)/trace_demo.o
+ $(CXX) $(CXXFLAGS) $^ -o $@
+ $(DSYMUTIL) $@
+
+sample_demo: $(OBJ)/sample_demo.o
+ $(CXX) $(CXXFLAGS) $^ -o $@
+ $(DSYMUTIL) $@
+
+memory_demo: $(OBJ)/memory_demo.o
+ $(CXX) $(CXXFLAGS) $^ -o $@
+ $(DSYMUTIL) $@
+
+# instr_demo.c is compiled with -finstrument-functions so every function calls
+# the enter/exit hooks; the trace runtime that records them is compiled WITHOUT
+# the flag (instrumenting the hooks themselves would recurse forever)
+$(OBJ)/instr_demo.o: instr_demo.c | $(OBJ)
+ $(CC) $(CFLAGS) -finstrument-functions -c $< -o $@
+
+$(OBJ)/hsdbg_trace.o: $(TRACE_RUNTIME) | $(OBJ)
+ $(CC) $(CFLAGS) -c $< -o $@
+
+instr_demo: $(OBJ)/instr_demo.o $(OBJ)/hsdbg_trace.o
+ $(CC) $(CFLAGS) $^ -o $@
+ $(DSYMUTIL) $@
+
+clean:
+ rm -rf $(TARGETS) *.dSYM $(OBJ)
+
+.PHONY: all clean
diff --git a/test/hello.cpp b/test/hello.cpp
new file mode 100644
index 0000000..9337000
--- /dev/null
+++ b/test/hello.cpp
@@ -0,0 +1,16 @@
+#include <print>
+
+auto add(int a, int b) -> int
+{
+ int result = a+b;
+ return result;
+}
+
+auto main() -> int
+{
+ int a = 50;
+ int b = 67;
+ int c = add(a, b);
+
+ std::println("{} + {} = {}", a, b, c);
+}
diff --git a/test/instr_demo.c b/test/instr_demo.c
new file mode 100644
index 0000000..7983b82
--- /dev/null
+++ b/test/instr_demo.c
@@ -0,0 +1,51 @@
+// a fully instrumented demo: built with -finstrument-functions so EVERY function
+// here is traced automatically, no hand-picking. kept in plain C with only
+// printf and nanosleep so the trace is exactly these functions
+#include <stdio.h>
+#include <time.h>
+
+static void nap(long ms)
+{
+ struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
+ nanosleep(&ts, NULL);
+}
+
+static long leaf(int n)
+{
+ long total = 0;
+ for (int i = 0; i < n * 40000; ++i)
+ total += i % 7;
+
+ return total;
+}
+
+static long inner(int round)
+{
+ long total = 0;
+ for (int i = 1; i <= 3; ++i)
+ total += leaf(round % 4 + i);
+
+ return total;
+}
+
+static long outer(int round)
+{
+ const long a = inner(round);
+ nap(20);
+ const long b = inner(round + 1);
+ return a + b;
+}
+
+int main(void)
+{
+ for (int round = 0; round < 15; ++round)
+ {
+ const long result = outer(round);
+ printf("round %d -> %ld\n", round, result);
+ fflush(stdout);
+ nap(120);
+ }
+
+ puts("done");
+ return 0;
+}
diff --git a/test/macro_demo.c b/test/macro_demo.c
new file mode 100644
index 0000000..072ec02
--- /dev/null
+++ b/test/macro_demo.c
@@ -0,0 +1,47 @@
+// a playground for the macro unwrapper. open this file in hsdbg's source view,
+// then hover any highlighted macro for a preview and click it to step through
+// the expansion one layer at a time in the macros panel.
+
+#include <stdio.h>
+
+// object-like macros that lean on each other, so unrolling takes real layers:
+// KIB -> (1 << 10), MIB -> (KIB * KIB) -> ((1 << 10) * (1 << 10)), and so on
+#define KIB (1 << 10)
+#define MIB (KIB * KIB)
+#define GIB (MIB * KIB)
+
+// function-like macros, including one built out of another
+#define SQUARE(x) ((x) * (x))
+#define CUBE(x) (SQUARE(x) * (x))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define CLAMP(v, lo, hi) MAX(lo, MIN(v, hi))
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+
+// the classic stringize-through-indirection: STR sees its argument raw, XSTR
+// expands it first. hover both to see "VERSION" vs "3"
+#define VERSION 3
+#define STR(x) #x
+#define XSTR(x) STR(x)
+
+// token pasting, direct and through a level of indirection
+#define CAT(a, b) a ## b
+#define XCAT(a, b) CAT(a, b)
+
+// a variadic logging macro
+#define LOG(level, fmt, ...) fprintf(stderr, "[" level "] " fmt "\n", __VA_ARGS__)
+
+int main(void)
+{
+ int width = 5;
+ int height = 9;
+
+ printf("a mebibyte is %d bytes\n", MIB);
+ printf("square %d, cube %d\n", SQUARE(width), CUBE(width));
+ printf("clamped %d\n", CLAMP(height, 0, 8));
+ printf("building version %s\n", XSTR(VERSION));
+
+ int XCAT(sum_, 1) = width + height;
+ LOG("info", "sum is %d", XCAT(sum_, 1));
+
+ return 0;
+}
diff --git a/test/memory_demo.cpp b/test/memory_demo.cpp
new file mode 100644
index 0000000..22c6b44
--- /dev/null
+++ b/test/memory_demo.cpp
@@ -0,0 +1,31 @@
+// a small demo target for the profiler: allocates and frees memory in slow
+// waves so the resident-memory graph has something to show
+#include <chrono>
+#include <cstdio>
+#include <thread>
+#include <vector>
+
+auto main() -> int
+{
+ std::vector<std::vector<char>> blocks;
+
+ for (int cycle = 0; cycle < 6; ++cycle)
+ {
+ // ramp up: grab ~40 MB over two seconds
+ for (int i = 0; i < 40; ++i)
+ {
+ blocks.emplace_back(1024 * 1024, static_cast<char>(i));
+ std::printf("cycle %d: allocated %zu MB\n", cycle, blocks.size());
+ std::fflush(stdout);
+ std::this_thread::sleep_for(std::chrono::milliseconds(50));
+ }
+
+ // hold for a moment, then release everything and start over
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+ blocks.clear();
+ blocks.shrink_to_fit();
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+ }
+
+ std::puts("done");
+}
diff --git a/test/sample_demo.cpp b/test/sample_demo.cpp
new file mode 100644
index 0000000..a3b1fa8
--- /dev/null
+++ b/test/sample_demo.cpp
@@ -0,0 +1,47 @@
+// a plain, un-instrumented target for the sampling profiler: NOTHING added to the
+// build, no flags, no linked runtime. functions do enough work (tens of ms) that
+// the sampler catches them. compile normally: clang++ -g sample_demo.cpp -o sample_demo
+#include <cstdio>
+
+// a spin of roughly the requested milliseconds of busywork
+static volatile long sink = 0;
+static void burn(int ms)
+{
+ // ~2 million iterations per ms is a rough guess; exact timing does not matter
+ for (long i = 0; i < ms * 800000L; ++i)
+ sink += i % 7;
+}
+
+static void deep(int round)
+{
+ burn(25 + round % 3 * 15);
+}
+
+static void phase_b(int round)
+{
+ burn(20);
+ deep(round);
+}
+
+static void phase_a(int round)
+{
+ burn(40);
+}
+
+int main()
+{
+ for (int round = 0; round < 400; ++round)
+ {
+ phase_a(round);
+ phase_b(round);
+
+ if (round % 10 == 0)
+ {
+ printf("round %d\n", round);
+ fflush(stdout);
+ }
+ }
+
+ puts("done");
+ return 0;
+}
diff --git a/test/trace_demo.cpp b/test/trace_demo.cpp
new file mode 100644
index 0000000..d5dbce0
--- /dev/null
+++ b/test/trace_demo.cpp
@@ -0,0 +1,49 @@
+// a demo target for the flame-chart timeline: a nest of functions that call one
+// another so the timeline has real depth. main is the outermost (bottom row),
+// each callee stacks a row higher
+#include <chrono>
+#include <cstdio>
+#include <thread>
+
+// innermost: a chunk of busywork so the leaf calls have visible width
+auto leaf(int n) -> long
+{
+ long total = 0;
+ for (int i = 0; i < n * 60000; ++i)
+ total += i % 7;
+
+ return total;
+}
+
+// calls leaf a few times
+auto inner(int round) -> long
+{
+ long total = 0;
+ for (int i = 1; i <= 3; ++i)
+ total += leaf(round % 4 + i);
+
+ return total;
+}
+
+// calls inner, one level below main
+auto outer(int round) -> long
+{
+ const long a = inner(round);
+ std::this_thread::sleep_for(std::chrono::milliseconds(30));
+ const long b = inner(round + 1);
+ return a + b;
+}
+
+auto main() -> int
+{
+ for (int round = 0; round < 20; ++round)
+ {
+ const long result = outer(round);
+ std::printf("round %d -> %ld\n", round, result);
+ std::fflush(stdout);
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(150));
+ }
+
+ std::puts("done");
+}