Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
3dbdd8d
chore: treat images as binary and ignore build and run output
fasharif Sep 25, 2026
c9888ee
refactor: move the sorts and the input generator into src/
fasharif Sep 25, 2026
6fb666f
feat: read RAPL energy from powercap, with wraparound and clear failures
fasharif Sep 25, 2026
a2c6575
feat: add the C worker and the one-line result protocol
fasharif Sep 25, 2026
e57293e
feat: add the measurement harness
fasharif Sep 25, 2026
21b932e
feat: add the Python worker and the Python tooling
fasharif Sep 25, 2026
020b6c6
feat: add the Node.js worker
fasharif Sep 25, 2026
a73319c
test: check cross-language parity and the harness end to end
fasharif Sep 25, 2026
d08f421
feat: add grid carbon intensity for the UAE and the UK
fasharif Sep 25, 2026
da4c52d
feat: add the statistics and report package
fasharif Sep 25, 2026
a625479
feat: run the whole study on bare-metal Linux with one command
fasharif Sep 25, 2026
243ebd2
ci: test every part of the study on every push
fasharif Sep 25, 2026
811cbb5
chore: mark the study script and the workers executable
fasharif Sep 25, 2026
feb8969
fix: keep coinciding lines and unplottable values visible in the charts
fasharif Sep 25, 2026
974f50a
feat: retry a worker process that fails, once by default
fasharif Sep 25, 2026
9839cba
chore: show timings in the local smoke report
fasharif Sep 25, 2026
982ec1c
docs: rewrite the README for greenbench and document the study
fasharif Sep 25, 2026
4925814
fix: make the report's provenance lines easier to read and reproduce
fasharif Sep 25, 2026
997da75
docs: generate results.md from a study-profile run, with timings pending
fasharif Sep 25, 2026
4b92622
fix: refuse RAPL counters that never move instead of recording zeros
fasharif Sep 25, 2026
beb98fe
fix: measure startup cost over a batch of launches, not one short pro…
fasharif Sep 26, 2026
0c681e9
fix: pin to two CPUs on different cores so Node.js helper threads can…
fasharif Sep 26, 2026
1868fab
fix: record a command that pastes back into a shell, and keep inputs …
fasharif Sep 26, 2026
004f350
feat: record CPU and memory limits and short sort loops in meta.json
fasharif Sep 26, 2026
4ca24d3
fix: give the Node.js sorts a packed array, as C and Python get
fasharif Sep 26, 2026
b151706
feat: answer the proxy question with power and ratios, not pooled rho
fasharif Sep 26, 2026
c8e8793
test: run moving, wrapping counters through the harness into the report
fasharif Sep 26, 2026
4f31e7c
fix: make the study script's checks catch what they claim to
fasharif Sep 26, 2026
59a6e7c
fix: pin the Docker base images by digest so Dependabot can update them
fasharif Sep 26, 2026
0ca001b
docs: cite the grid intensity data the way Our World in Data asks
fasharif Sep 26, 2026
ca63170
docs: describe the study as it now measures and decides
fasharif Sep 26, 2026
b8b4407
docs: give the statistics feature a plain name
fasharif Sep 26, 2026
ae844dc
docs: regenerate results.md with harness 2.1, timings still pending
fasharif Sep 26, 2026
9b2b83f
fix: judge each socket's counter on its own, and refuse implausible j…
fasharif Sep 26, 2026
037dbf5
test: simulate RAPL readings from the clock so energy tests cannot flake
fasharif Sep 27, 2026
fde62e7
feat: give rank agreement for each language and algorithm
fasharif Sep 27, 2026
944b88a
feat: read each power ratio against a margin set before measuring
fasharif Sep 27, 2026
c8a8900
fix: aim calibration 20% above the minimum loop length
fasharif Sep 27, 2026
4721180
fix: refuse --cpus when the kernel leaves out a listed CPU
fasharif Sep 27, 2026
cf4aa73
feat: generate the README's results block with the report
fasharif Sep 27, 2026
27629e6
fix: keep the study off CPU 0's sibling thread and efficiency cores
fasharif Sep 27, 2026
45aeec1
fix: block suspend while the study measures
fasharif Sep 27, 2026
a940a04
build: add make setup, so the quick start is five commands
fasharif Sep 27, 2026
f66878c
ci: add job timeouts, test the Node.js worker on 18 and 20, drop a re…
fasharif Sep 27, 2026
60ddc84
docs: keep the README and study notes true before and after the study
fasharif Sep 27, 2026
a48caef
docs: drop an unmeasured claim about language speed, and rewrap
fasharif Sep 27, 2026
1295633
docs: regenerate results.md with harness 2.2.0, timings still pending
fasharif Sep 27, 2026
177f578
feat: report time and time ratios from a run without energy
fasharif Oct 2, 2026
9e55f7b
feat: measure time for the study profile in Docker, with energy off
fasharif Oct 2, 2026
59cccae
docs: publish time results measured in Docker, with energy pending
fasharif Oct 2, 2026
bbe7bab
docs: explain why the C bubble sort is slower than Node.js
fasharif Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
build
results
.venv
**/node_modules
**/__pycache__
.mypy_cache
.ruff_cache
.pytest_cache
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
* text=auto eol=lf

*.png binary
*.jpg binary
*.gif binary
*.pdf binary
27 changes: 27 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly

- package-ecosystem: pip
directory: /
schedule:
interval: weekly
groups:
python:
patterns: ["*"]

- package-ecosystem: npm
directory: /workers/node
schedule:
interval: weekly
groups:
node-dev-tools:
patterns: ["*"]

- package-ecosystem: docker
directory: /
schedule:
interval: weekly
128 changes: 110 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ permissions:
contents: read

jobs:
test:
name: Build and test (${{ matrix.compiler }})
c:
name: C build and unit tests (${{ matrix.compiler }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
Expand All @@ -22,44 +23,135 @@ jobs:
- name: Build with warnings as errors
run: make CC=${{ matrix.compiler }}

- name: Unit tests
- name: Unit tests (sorts, inputs, RAPL parsing and wraparound, options, plan, protocol, output, spawn)
run: make CC=${{ matrix.compiler }} test

sanitizers:
name: AddressSanitizer and UBSan
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7

# Recent kernels use more address-space randomisation than older sanitizer runtimes expect.
- name: Reduce ASLR entropy for the sanitizer runtime
run: sudo sysctl vm.mmap_rnd_bits=28

- name: Tests and a short benchmark under sanitizers
- name: Unit tests and a short harness run with a fake powercap tree, under sanitizers
run: make sanitize

benchmark:
name: Benchmark and charts
workers:
name: Cross-language parity, harness run and pipeline
runs-on: ubuntu-latest
needs: test
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
cache-dependency-path: requirements-dev.txt

- uses: actions/setup-node@v7
with:
node-version: "24"

- name: Install the Python tools
run: python -m pip install --require-hashes -r requirements-dev.txt

- name: Build
run: make

- name: Run the benchmark
run: ./energy_explorer | tee data.csv
# Includes the parity tests: xorshift32, inputs and operation counts identical in C,
# Python and Node.js. make parity runs them on their own.
- name: Parity, harness, study script and pipeline integration tests
run: make integration PYTHON=python

- name: Draw the charts
run: |
python3 -m venv .venv
.venv/bin/pip install --quiet -r requirements.txt
.venv/bin/python visualize.py data.csv
- name: Short harness run with energy off, then the report
run: make smoke PYTHON=python

- uses: actions/upload-artifact@v7
with:
name: benchmark-results
path: |
data.csv
*.png
name: smoke-run
path: results/smoke/

statistics:
name: Statistics and Python worker (Python ${{ matrix.python }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python: ["3.12", "3.14"]
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python }}
cache: pip
cache-dependency-path: requirements-dev.txt

- name: Install the Python tools
run: python -m pip install --require-hashes -r requirements-dev.txt

- name: Lint, type-check and test
run: make check-python PYTHON=python

node:
name: Node.js worker (lint, type-check, tests)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
cache-dependency-path: workers/node/package-lock.json

- name: Lint, type-check and test
run: make check-node

node-older:
name: Node.js worker tests on Node.js ${{ matrix.node }}
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
# The workers support Node.js 18 and later (package.json engines), which is what
# Ubuntu 24.04 ships; the lint and type-check tools need a newer Node.js.
node: ["18", "20"]
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}

- name: Worker tests (no packages needed)
working-directory: workers/node
run: node --test

shell:
name: ShellCheck
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7

- name: Lint the scripts
run: make lint-shell

actionlint:
name: Workflow lint
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7

- name: actionlint
run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.12 -color
20 changes: 14 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# Build output and benchmark results
*.o
/energy_explorer
/tests/test_sorts
/data.csv
/*.png
# Build output
/build/

# Harness runs made for development, smoke tests and rehearsals. Study data goes in data/runs/.
/results/

# Python
/.venv/
__pycache__/
.mypy_cache/
.ruff_cache/
.pytest_cache/

# Node.js
node_modules/
25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# A Linux environment with everything greenbench needs: GCC, Clang, make, Python 3.12,
# Node.js 24 and ShellCheck. RAPL counters are not visible inside a container, so this image
# runs the harness with energy off. It is for trying the project on Windows or macOS and for
# running the same checks as CI; the energy study itself needs bare-metal Linux.
#
# The base images are pinned by digest, and Dependabot proposes updates. If Docker Hub limits
# pulls, build from Amazon's mirror of the Docker official images, which serves the same
# digests. Each --build-context name must match a FROM line exactly, digest included, so this
# command derives them from the FROM lines (in bash, from the repository root):
# docker buildx build $(sed -n 's|^FROM \([^ ]*\).*|--build-context \1=docker-image://public.ecr.aws/docker/library/\1|p' Dockerfile) -t greenbench .
FROM node:24-trixie-slim@sha256:8ec5d7557396cfe32d21c3f9c13072355ceab22b584578ca4bb28af31120cffe AS node

FROM python:3.12-slim-trixie@sha256:f77ac9e44ae96ef2c90b8053ea08c31f8be030f824196b0ae4db6d462c84e51f
RUN apt-get update \
&& apt-get install -y --no-install-recommends clang gcc libc6-dev make shellcheck \
&& rm -rf /var/lib/apt/lists/*
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
COPY requirements-dev.txt /tmp/requirements-dev.txt
RUN pip install --no-cache-dir --require-hashes -r /tmp/requirements-dev.txt
WORKDIR /work
COPY . .
CMD ["make", "smoke"]
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025-2026 Farah Sharif
Copyright (c) 2026 Farah Sharif

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
111 changes: 89 additions & 22 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,40 +1,107 @@
CC ?= cc
CFLAGS ?= -O2
LDFLAGS ?=
WARNINGS := -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Werror
ALL_CFLAGS = -std=c11 $(WARNINGS) $(CFLAGS)

NAME := energy_explorer
TEST_BIN := tests/test_sorts
BUILD ?= build
# Use the project's virtual environment when there is one (see README), else python3.
PYTHON ?= $(if $(wildcard .venv/bin/python),.venv/bin/python,python3)
NPM ?= npm
SANITIZE := -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer

.PHONY: all test sanitize results clean fclean re
# Runs the analysis package from analysis/ without installing it.
PYRUN := PYTHONPATH=analysis $(PYTHON)

LIB := sorts inputs rapl catalog options plan protocol output spawn timing
LIB_OBJ := $(LIB:%=$(BUILD)/obj/%.o)
HEADERS := $(wildcard src/*.h)
HARNESS := $(BUILD)/greenbench
WORKER := $(BUILD)/greenbench-worker
# A test build of the harness whose RAPL readings are computed from the clock (see src/rapl.c),
# so that energy tests do not depend on another program being scheduled to move fake counters.
SIMULATED := $(BUILD)/greenbench-simulated
TESTS := test_sorts test_inputs test_rapl test_options test_plan test_protocol test_output test_spawn
TEST_BIN := $(TESTS:%=$(BUILD)/tests/%)

.PHONY: all setup simulated test sanitize smoke parity integration check-python check-node lint-shell check clean study

all: $(HARNESS) $(WORKER)

simulated: $(SIMULATED)

all: $(NAME)
# The project's virtual environment with the analysis packages, installed from hashed pins.
# Afterwards the Makefile runs Python from .venv.
setup:
python3 -m venv .venv
.venv/bin/python -m pip install --require-hashes -r requirements.txt

$(NAME): main.o sorts.o
$(CC) $(ALL_CFLAGS) $(LDFLAGS) -o $@ main.o sorts.o
$(BUILD)/obj $(BUILD)/tests:
mkdir -p $@

%.o: %.c sorts.h
$(BUILD)/obj/%.o: src/%.c $(HEADERS) | $(BUILD)/obj
$(CC) $(ALL_CFLAGS) -c -o $@ $<

$(TEST_BIN): tests/test_sorts.c sorts.c sorts.h
$(CC) $(ALL_CFLAGS) -I. $(LDFLAGS) -o $@ tests/test_sorts.c sorts.c
$(BUILD)/obj/fixture.o: tests/fixture.c tests/fixture.h | $(BUILD)/obj
$(CC) $(ALL_CFLAGS) -c -o $@ $<

$(HARNESS): $(BUILD)/obj/greenbench.o $(LIB_OBJ)
$(CC) $(ALL_CFLAGS) $(LDFLAGS) -o $@ $^

$(WORKER): $(BUILD)/obj/worker.o $(BUILD)/obj/sorts.o $(BUILD)/obj/inputs.o $(BUILD)/obj/protocol.o
$(CC) $(ALL_CFLAGS) $(LDFLAGS) -o $@ $^

$(BUILD)/obj/%-simulated.o: src/%.c $(HEADERS) | $(BUILD)/obj
$(CC) $(ALL_CFLAGS) -DGREENBENCH_SIMULATED_RAPL -c -o $@ $<

$(SIMULATED): $(BUILD)/obj/greenbench-simulated.o $(BUILD)/obj/rapl-simulated.o $(filter-out %/rapl.o,$(LIB_OBJ))
$(CC) $(ALL_CFLAGS) $(LDFLAGS) -o $@ $^

$(BUILD)/tests/%: tests/%.c tests/check.h $(LIB_OBJ) $(BUILD)/obj/fixture.o $(HEADERS) | $(BUILD)/tests
$(CC) $(ALL_CFLAGS) -Isrc -Itests $(LDFLAGS) -o $@ $< $(LIB_OBJ) $(BUILD)/obj/fixture.o

# C unit tests. Run from the repository root: some read tests/fixtures.
test: $(TEST_BIN)
./$(TEST_BIN)
@for t in $(TEST_BIN); do ./$$t || exit 1; done

sanitize: fclean
$(MAKE) test all CFLAGS="$(SANITIZE)" LDFLAGS="$(SANITIZE)"
./$(NAME) 100 300 > /dev/null
# Unit tests and a short C-only harness run under AddressSanitizer and UBSan. The run uses the
# simulated-RAPL build on a fake two-socket tree, so the energy and idle-baseline code runs too.
sanitize:
$(MAKE) BUILD=build/sanitize CFLAGS="$(SANITIZE)" LDFLAGS="$(SANITIZE)" all simulated test
$(PYTHON) tests/integration/powercap.py build tests/fixtures/powercap/server-two-socket.txt build/sanitize/powercap
build/sanitize/greenbench-simulated run --profile smoke --languages c --quiet --energy require \
--powercap-root build/sanitize/powercap --out build/sanitize/smoke

results: $(NAME)
./$(NAME) > data.csv
python3 visualize.py data.csv
# A short run of every language with energy off, then the report. Works anywhere.
smoke: all
$(HARNESS) run --profile smoke --energy off --out results/smoke
$(PYRUN) -m greenbench_analysis report --runs results/smoke/runs.csv --out results/smoke/results.md \
--img-dir results/smoke/img

clean:
rm -f *.o $(TEST_BIN)
# The three workers must generate identical sequences and inputs and do identical work.
parity: all
$(PYRUN) -m pytest -q tests/integration/test_parity.py

# End-to-end tests of the harness binary, the workers and the report.
integration: all simulated
$(PYRUN) -m pytest -q tests/integration

check-python:
$(PYRUN) -m ruff check .
$(PYRUN) -m ruff format --check .
$(PYRUN) -m mypy
$(PYRUN) -m pytest -q analysis/tests workers/python/tests

fclean: clean
rm -f $(NAME) data.csv comparisons.png time.png orders.png
check-node:
cd workers/node && $(NPM) ci --no-audit --no-fund && $(NPM) run lint && $(NPM) run typecheck && $(NPM) test

re: fclean all
lint-shell:
shellcheck scripts/*.sh

check: all test check-python check-node integration

# The full study on bare-metal Linux; see docs/bare-metal.md.
study:
./scripts/run-study.sh

clean:
rm -rf build results/smoke
Loading
Loading