From 2bb24ef0e7027105262c2468056beeae4d634bc3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Dec 2025 22:53:08 +0000 Subject: [PATCH 1/2] Add HTTP server throughput benchmark Add a real-world benchmark measuring HTTP server request throughput: - HTTP server implementations for C, Python, JavaScript, Ruby, and Hemlock - Python-based client for measuring requests/second - Integration with run.sh benchmark runner - Reports requests/second with ratio comparison vs C baseline This tests real-world I/O performance and networking capabilities. --- CLAUDE.md | 1 + benchmarks.json | 12 ++ benchmarks/http_throughput/http_client.py | 66 +++++++ benchmarks/http_throughput/http_throughput.c | 100 ++++++++++ .../http_throughput/http_throughput.hml | 36 ++++ benchmarks/http_throughput/http_throughput.js | 41 ++++ benchmarks/http_throughput/http_throughput.py | 43 +++++ benchmarks/http_throughput/http_throughput.rb | 58 ++++++ run.sh | 180 ++++++++++++++++-- 9 files changed, 521 insertions(+), 16 deletions(-) create mode 100644 benchmarks/http_throughput/http_client.py create mode 100644 benchmarks/http_throughput/http_throughput.c create mode 100644 benchmarks/http_throughput/http_throughput.hml create mode 100644 benchmarks/http_throughput/http_throughput.js create mode 100644 benchmarks/http_throughput/http_throughput.py create mode 100644 benchmarks/http_throughput/http_throughput.rb diff --git a/CLAUDE.md b/CLAUDE.md index bdfbf3d..b140925 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,7 @@ HEMLOCKC_BIN="../hemlock/hemlockc" ./run.sh - **array_sum** - Sum large array (memory, iteration) - **string_concat** - String concatenation (allocation, GC pressure) - **primes_sieve** - Sieve of Eratosthenes (array access, loops) +- **http_throughput** - HTTP server request throughput (real-world I/O, networking) ## Notes diff --git a/benchmarks.json b/benchmarks.json index 288ad83..17c2d1d 100644 --- a/benchmarks.json +++ b/benchmarks.json @@ -19,6 +19,13 @@ "description": "Sieve of Eratosthenes", "default_input": 1000000, "quick_input": 100000 + }, + "http_throughput": { + "description": "HTTP server request throughput", + "default_input": 2000, + "quick_input": 500, + "metric": "requests_per_second", + "higher_is_better": true } }, "languages": { @@ -46,6 +53,11 @@ "extension": ".js", "compile": null, "run": "node {src} {input}" + }, + "ruby": { + "extension": ".rb", + "compile": null, + "run": "ruby {src} {input}" } } } diff --git a/benchmarks/http_throughput/http_client.py b/benchmarks/http_throughput/http_client.py new file mode 100644 index 0000000..83275cc --- /dev/null +++ b/benchmarks/http_throughput/http_client.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +HTTP throughput benchmark client. +Makes N requests to a server and reports requests/second. +Uses one connection per request for consistent benchmarking. +""" + +import sys +import time +import socket +import argparse + +def make_requests(host, port, num_requests): + """Make HTTP requests and return elapsed time in seconds.""" + request = ( + f"GET / HTTP/1.1\r\n" + f"Host: {host}:{port}\r\n" + f"Connection: close\r\n" + "\r\n" + ).encode() + + start = time.perf_counter() + completed = 0 + + for _ in range(num_requests): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(5.0) + sock.connect((host, port)) + sock.sendall(request) + + # Read full response until connection closes + response = b"" + while True: + chunk = sock.recv(4096) + if not chunk: + break + response += chunk + + sock.close() + completed += 1 + except (socket.error, socket.timeout) as e: + # Skip failed requests + pass + + elapsed = time.perf_counter() - start + return completed, elapsed + +def main(): + parser = argparse.ArgumentParser(description='HTTP throughput benchmark client') + parser.add_argument('--host', default='127.0.0.1', help='Server host') + parser.add_argument('--port', type=int, required=True, help='Server port') + parser.add_argument('--requests', type=int, default=10000, help='Number of requests') + + args = parser.parse_args() + + completed, elapsed = make_requests(args.host, args.port, args.requests) + + if completed > 0: + rps = completed / elapsed + print(f"{rps:.2f}") + else: + print("0.00") + +if __name__ == '__main__': + main() diff --git a/benchmarks/http_throughput/http_throughput.c b/benchmarks/http_throughput/http_throughput.c new file mode 100644 index 0000000..3ec5b71 --- /dev/null +++ b/benchmarks/http_throughput/http_throughput.c @@ -0,0 +1,100 @@ +// HTTP throughput benchmark - C implementation +// Simple HTTP server using raw sockets + +#include +#include +#include +#include +#include +#include +#include + +#define BUFFER_SIZE 4096 + +static const char *RESPONSE = + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 27\r\n" + "Connection: close\r\n" + "\r\n" + "{\"message\":\"Hello World!\"}"; + +static int server_fd = -1; +static volatile int running = 1; + +void handle_signal(int sig) { + (void)sig; + running = 0; + if (server_fd != -1) { + close(server_fd); + } +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + int port = atoi(argv[1]); + struct sockaddr_in address; + int opt = 1; + char buffer[BUFFER_SIZE]; + + signal(SIGTERM, handle_signal); + signal(SIGINT, handle_signal); + + if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { + perror("socket failed"); + return 1; + } + + if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { + perror("setsockopt"); + return 1; + } + + address.sin_family = AF_INET; + address.sin_addr.s_addr = INADDR_ANY; + address.sin_port = htons(port); + + if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { + perror("bind failed"); + return 1; + } + + if (listen(server_fd, 128) < 0) { + perror("listen"); + return 1; + } + + // Signal ready by printing port + printf("READY %d\n", port); + fflush(stdout); + + size_t response_len = strlen(RESPONSE); + + while (running) { + socklen_t addrlen = sizeof(address); + int client_fd = accept(server_fd, (struct sockaddr *)&address, &addrlen); + if (client_fd < 0) { + if (!running) break; + continue; + } + + // Read request + ssize_t bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1); + if (bytes_read > 0) { + buffer[bytes_read] = '\0'; + // Simple check for HTTP request + if (strncmp(buffer, "GET ", 4) == 0) { + write(client_fd, RESPONSE, response_len); + } + } + + close(client_fd); + } + + close(server_fd); + return 0; +} diff --git a/benchmarks/http_throughput/http_throughput.hml b/benchmarks/http_throughput/http_throughput.hml new file mode 100644 index 0000000..94978ae --- /dev/null +++ b/benchmarks/http_throughput/http_throughput.hml @@ -0,0 +1,36 @@ +// HTTP throughput benchmark - Hemlock implementation +// Uses Hemlock's built-in HTTP server support + +fn main() { + let port_str = args[1]; + let port = parse_int(port_str); + + // Create HTTP server + let server = http_server(port); + + // Register route handler + server.on("GET", "/", fn(req, res) { + res.json({ message: "Hello World!" }); + }); + + // Signal ready + print("READY " + port_str); + + // Start server (blocks) + server.listen(); +} + +fn parse_int(s) { + let result = 0; + let i = 0; + while (i < s.length) { + let ch = s[i]; + let code: i32 = ch; + let digit = code - 48; + result = result * 10 + digit; + i = i + 1; + } + return result; +} + +main(); diff --git a/benchmarks/http_throughput/http_throughput.js b/benchmarks/http_throughput/http_throughput.js new file mode 100644 index 0000000..618923f --- /dev/null +++ b/benchmarks/http_throughput/http_throughput.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node +// HTTP throughput benchmark - JavaScript (Node.js) implementation + +const http = require('http'); + +const port = parseInt(process.argv[2]); + +if (!port) { + console.error('Usage: node http_throughput.js '); + process.exit(1); +} + +const response = JSON.stringify({ message: 'Hello World!' }); +const headers = { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(response), + 'Connection': 'close' +}; + +const server = http.createServer((req, res) => { + if (req.method === 'GET') { + res.writeHead(200, headers); + res.end(response); + } else { + res.writeHead(405); + res.end(); + } +}); + +server.listen(port, () => { + // Signal ready + console.log(`READY ${port}`); +}); + +process.on('SIGTERM', () => { + server.close(() => process.exit(0)); +}); + +process.on('SIGINT', () => { + server.close(() => process.exit(0)); +}); diff --git a/benchmarks/http_throughput/http_throughput.py b/benchmarks/http_throughput/http_throughput.py new file mode 100644 index 0000000..c8a602e --- /dev/null +++ b/benchmarks/http_throughput/http_throughput.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# HTTP throughput benchmark - Python implementation + +import sys +import signal +from http.server import HTTPServer, BaseHTTPRequestHandler +import json + +class BenchmarkHandler(BaseHTTPRequestHandler): + # Disable logging for performance + def log_message(self, format, *args): + pass + + def do_GET(self): + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Connection', 'close') + response = b'{"message":"Hello World!"}' + self.send_header('Content-Length', len(response)) + self.end_headers() + self.wfile.write(response) + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + port = int(sys.argv[1]) + server = HTTPServer(('', port), BenchmarkHandler) + + def shutdown(signum, frame): + server.shutdown() + + signal.signal(signal.SIGTERM, shutdown) + signal.signal(signal.SIGINT, shutdown) + + # Signal ready + print(f"READY {port}", flush=True) + + server.serve_forever() + +if __name__ == '__main__': + main() diff --git a/benchmarks/http_throughput/http_throughput.rb b/benchmarks/http_throughput/http_throughput.rb new file mode 100644 index 0000000..f482e5e --- /dev/null +++ b/benchmarks/http_throughput/http_throughput.rb @@ -0,0 +1,58 @@ +#!/usr/bin/env ruby +# HTTP throughput benchmark - Ruby implementation + +require 'socket' + +port = ARGV[0]&.to_i + +if port.nil? || port == 0 + STDERR.puts "Usage: ruby http_throughput.rb " + exit 1 +end + +response_body = '{"message":"Hello World!"}' +response = "HTTP/1.1 200 OK\r\n" \ + "Content-Type: application/json\r\n" \ + "Content-Length: #{response_body.length}\r\n" \ + "Connection: close\r\n" \ + "\r\n" \ + "#{response_body}" + +server = TCPServer.new('0.0.0.0', port) +server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) + +running = true +trap('TERM') { running = false; server.close rescue nil } +trap('INT') { running = false; server.close rescue nil } + +# Signal ready +puts "READY #{port}" +STDOUT.flush + +while running + begin + client = server.accept + + # Read request line + request = client.gets + next if request.nil? + + # Read remaining headers + while (line = client.gets) && line != "\r\n" + # Skip headers + end + + if request.start_with?('GET ') + client.write(response) + end + + client.close rescue nil + rescue IOError, Errno::EBADF + break + rescue => e + # Connection errors are expected during shutdown + break unless running + end +end + +server.close rescue nil diff --git a/run.sh b/run.sh index 414b181..33429a0 100755 --- a/run.sh +++ b/run.sh @@ -28,7 +28,7 @@ usage() { echo " --iter N Number of iterations (default: 3)" echo " --help, -h Show this help" echo "" - echo "Benchmarks: fib, array_sum, string_concat, primes_sieve" + echo "Benchmarks: fib, array_sum, string_concat, primes_sieve, http_throughput" echo " (leave empty to run all)" } @@ -72,6 +72,10 @@ get_input() { primes_sieve) [[ $QUICK_MODE -eq 1 ]] && echo 100000 || echo 1000000 ;; + http_throughput) + # Number of requests (keep low due to connection overhead) + [[ $QUICK_MODE -eq 1 ]] && echo 500 || echo 2000 + ;; esac } @@ -179,6 +183,126 @@ run_benchmark() { echo $((sum / count)) } +# Run HTTP throughput benchmark (special handling - measures requests/second) +run_http_benchmark() { + local lang=$1 + local num_requests=$2 + local bench_dir="$SCRIPT_DIR/benchmarks/http_throughput" + local client="$bench_dir/http_client.py" + local port=8765 + local sum=0 + local count=0 + local server_pid + + # Find an available port + while netstat -tuln 2>/dev/null | grep -q ":$port " || ss -tuln 2>/dev/null | grep -q ":$port "; do + port=$((port + 1)) + done + + for ((i=0; i/dev/null || return 1 + "$bin" "$port" & + server_pid=$! + ;; + hemlock) + local src="$bench_dir/http_throughput.hml" + [[ ! -f "$src" ]] && return 1 + local hemlock_bin="${HEMLOCK_BIN:-hemlock}" + "$hemlock_bin" "$src" "$port" & + server_pid=$! + ;; + hemlockc) + local src="$bench_dir/http_throughput.hml" + local bin="$BUILD_DIR/http_throughput_hemlockc" + [[ ! -f "$src" ]] && return 1 + local hemlockc_bin="${HEMLOCKC_BIN:-hemlockc}" + local runtime_dir="${HEMLOCK_RUNTIME:-}" + if [[ -z "$runtime_dir" ]]; then + for dir in "$SCRIPT_DIR/../hemlock" "$HOME/Projects/hemlock"; do + if [[ -f "$dir/libhemlock_runtime.so" ]]; then + runtime_dir="$dir" + break + fi + done + fi + if [[ -n "$runtime_dir" ]]; then + export C_INCLUDE_PATH="${runtime_dir}/runtime/include:${C_INCLUDE_PATH:-}" + export LIBRARY_PATH="${runtime_dir}:${LIBRARY_PATH:-}" + export LD_LIBRARY_PATH="${runtime_dir}:${LD_LIBRARY_PATH:-}" + fi + "$hemlockc_bin" -O3 "$src" -o "$bin" 2>/dev/null || return 1 + "$bin" "$port" & + server_pid=$! + ;; + python) + local src="$bench_dir/http_throughput.py" + [[ ! -f "$src" ]] && return 1 + python3 "$src" "$port" & + server_pid=$! + ;; + javascript) + local src="$bench_dir/http_throughput.js" + [[ ! -f "$src" ]] && return 1 + node "$src" "$port" & + server_pid=$! + ;; + ruby) + local src="$bench_dir/http_throughput.rb" + [[ ! -f "$src" ]] && return 1 + ruby "$src" "$port" & + server_pid=$! + ;; + *) + return 1 + ;; + esac + + # Wait for server to be ready (max 5 seconds) + local ready=0 + for ((j=0; j<50; j++)); do + if curl -s --max-time 1 "http://127.0.0.1:$port/" >/dev/null 2>&1; then + ready=1 + break + fi + sleep 0.1 + done + + if [[ $ready -eq 0 ]]; then + kill "$server_pid" 2>/dev/null + wait "$server_pid" 2>/dev/null + return 1 + fi + + # Run client and get requests/second + local rps + rps=$(python3 "$client" --port "$port" --requests "$num_requests" 2>/dev/null) + + # Stop server + kill "$server_pid" 2>/dev/null + wait "$server_pid" 2>/dev/null + + if [[ -n "$rps" ]]; then + # Convert to integer for averaging (multiply by 100 for precision) + local rps_int=$(echo "$rps * 100" | bc | cut -d. -f1) + sum=$((sum + rps_int)) + count=$((count + 1)) + fi + + # Small delay between iterations + sleep 0.2 + done + + [[ $count -eq 0 ]] && return 1 + # Return average (divide by 100 to get back to original scale) + echo "scale=2; $sum / $count / 100" | bc +} + # Format time nicely format_time() { local ms=$1 @@ -195,7 +319,7 @@ run_all() { if [[ -n "$BENCHMARK" ]]; then benchmarks="$BENCHMARK" else - benchmarks="fib array_sum string_concat primes_sieve" + benchmarks="fib array_sum string_concat primes_sieve http_throughput" fi local languages="c hemlockc hemlock python javascript ruby" @@ -208,25 +332,49 @@ run_all() { for bench in $benchmarks; do local input=$(get_input "$bench") - echo -e "${BOLD}${BLUE}$bench${NC} (n=$input)" - echo "─────────────────────────────────" - local c_time=0 + if [[ "$bench" == "http_throughput" ]]; then + echo -e "${BOLD}${BLUE}$bench${NC} (requests=$input)" + echo "─────────────────────────────────" - for lang in $languages; do - local time_ms - time_ms=$(run_benchmark "$bench" "$lang" "$input" 2>/dev/null) || continue + local c_rps=0 - [[ $lang == "c" ]] && c_time=$time_ms + for lang in $languages; do + local rps + rps=$(run_http_benchmark "$lang" "$input" 2>/dev/null) || continue - local formatted=$(format_time "$time_ms") - local ratio="" - if [[ $c_time -gt 0 && $lang != "c" ]]; then - ratio=$(printf " (%.1fx)" "$(echo "scale=1; $time_ms / $c_time" | bc)") - fi + [[ $lang == "c" ]] && c_rps=$(echo "$rps" | bc) - printf " %-12s %8s%s\n" "$lang" "$formatted" "$ratio" - done + local formatted=$(printf "%.0f req/s" "$rps") + local ratio="" + if [[ $(echo "$c_rps > 0" | bc) -eq 1 && $lang != "c" ]]; then + # For throughput, higher is better, so ratio < 1 means slower + ratio=$(printf " (%.2fx)" "$(echo "scale=2; $rps / $c_rps" | bc)") + fi + + printf " %-12s %12s%s\n" "$lang" "$formatted" "$ratio" + done + else + echo -e "${BOLD}${BLUE}$bench${NC} (n=$input)" + echo "─────────────────────────────────" + + local c_time=0 + + for lang in $languages; do + local time_ms + time_ms=$(run_benchmark "$bench" "$lang" "$input" 2>/dev/null) || continue + + [[ $lang == "c" ]] && c_time=$time_ms + + local formatted=$(format_time "$time_ms") + local ratio="" + if [[ $c_time -gt 0 && $lang != "c" ]]; then + ratio=$(printf " (%.1fx)" "$(echo "scale=1; $time_ms / $c_time" | bc)") + fi + + printf " %-12s %8s%s\n" "$lang" "$formatted" "$ratio" + done + fi echo "" done } From 723a6ff30434faf5d7efe72e4c02cb1965e476fb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Dec 2025 12:25:03 +0000 Subject: [PATCH 2/2] Add JSON parse/serialize benchmark Tests JSON parsing and serialization performance: - C: Custom minimal JSON parser - Python: json module (built-in) - JavaScript: JSON.parse/stringify (V8 optimized) - Ruby: JSON module - Hemlock: json_parse/json_stringify Parses a sample JSON object with nested structures and arrays, then serializes back to string. Measures parsing, object creation, and string generation performance. --- CLAUDE.md | 1 + benchmarks.json | 5 + benchmarks/json_parse/json_parse.c | 208 +++++++++++++++++++++++++++ benchmarks/json_parse/json_parse.hml | 35 +++++ benchmarks/json_parse/json_parse.js | 36 +++++ benchmarks/json_parse/json_parse.py | 40 ++++++ benchmarks/json_parse/json_parse.rb | 40 ++++++ run.sh | 7 +- 8 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 benchmarks/json_parse/json_parse.c create mode 100644 benchmarks/json_parse/json_parse.hml create mode 100644 benchmarks/json_parse/json_parse.js create mode 100644 benchmarks/json_parse/json_parse.py create mode 100644 benchmarks/json_parse/json_parse.rb diff --git a/CLAUDE.md b/CLAUDE.md index b140925..677d27e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,7 @@ HEMLOCKC_BIN="../hemlock/hemlockc" ./run.sh - **array_sum** - Sum large array (memory, iteration) - **string_concat** - String concatenation (allocation, GC pressure) - **primes_sieve** - Sieve of Eratosthenes (array access, loops) +- **json_parse** - JSON parsing and serialization (string processing, object creation) - **http_throughput** - HTTP server request throughput (real-world I/O, networking) ## Notes diff --git a/benchmarks.json b/benchmarks.json index 17c2d1d..93ab157 100644 --- a/benchmarks.json +++ b/benchmarks.json @@ -20,6 +20,11 @@ "default_input": 1000000, "quick_input": 100000 }, + "json_parse": { + "description": "JSON parse and serialize", + "default_input": 100000, + "quick_input": 10000 + }, "http_throughput": { "description": "HTTP server request throughput", "default_input": 2000, diff --git a/benchmarks/json_parse/json_parse.c b/benchmarks/json_parse/json_parse.c new file mode 100644 index 0000000..ed4f5f0 --- /dev/null +++ b/benchmarks/json_parse/json_parse.c @@ -0,0 +1,208 @@ +// JSON parse/serialize benchmark - C implementation +// Minimal JSON parser for benchmarking (handles objects, arrays, strings, numbers, bools, null) + +#include +#include +#include +#include + +typedef enum { JSON_NULL, JSON_BOOL, JSON_NUMBER, JSON_STRING, JSON_ARRAY, JSON_OBJECT } JsonType; + +typedef struct JsonValue JsonValue; +typedef struct JsonPair JsonPair; + +struct JsonValue { + JsonType type; + union { + int bool_val; + double num_val; + char *str_val; + struct { JsonValue **items; int count; int capacity; } array; + struct { JsonPair *pairs; int count; int capacity; } object; + }; +}; + +struct JsonPair { + char *key; + JsonValue *value; +}; + +static const char *json_input; +static int json_pos; + +static void skip_whitespace(void) { + while (json_input[json_pos] && isspace(json_input[json_pos])) json_pos++; +} + +static JsonValue *parse_value(void); + +static char *parse_string_raw(void) { + if (json_input[json_pos] != '"') return NULL; + json_pos++; + int start = json_pos; + while (json_input[json_pos] && json_input[json_pos] != '"') { + if (json_input[json_pos] == '\\') json_pos++; + json_pos++; + } + int len = json_pos - start; + char *s = malloc(len + 1); + memcpy(s, json_input + start, len); + s[len] = '\0'; + json_pos++; // skip closing quote + return s; +} + +static JsonValue *parse_string(void) { + JsonValue *v = malloc(sizeof(JsonValue)); + v->type = JSON_STRING; + v->str_val = parse_string_raw(); + return v; +} + +static JsonValue *parse_number(void) { + JsonValue *v = malloc(sizeof(JsonValue)); + v->type = JSON_NUMBER; + char *end; + v->num_val = strtod(json_input + json_pos, &end); + json_pos = end - json_input; + return v; +} + +static JsonValue *parse_array(void) { + JsonValue *v = malloc(sizeof(JsonValue)); + v->type = JSON_ARRAY; + v->array.items = NULL; + v->array.count = 0; + v->array.capacity = 0; + json_pos++; // skip [ + skip_whitespace(); + while (json_input[json_pos] != ']') { + if (v->array.count >= v->array.capacity) { + v->array.capacity = v->array.capacity ? v->array.capacity * 2 : 8; + v->array.items = realloc(v->array.items, v->array.capacity * sizeof(JsonValue*)); + } + v->array.items[v->array.count++] = parse_value(); + skip_whitespace(); + if (json_input[json_pos] == ',') { json_pos++; skip_whitespace(); } + } + json_pos++; // skip ] + return v; +} + +static JsonValue *parse_object(void) { + JsonValue *v = malloc(sizeof(JsonValue)); + v->type = JSON_OBJECT; + v->object.pairs = NULL; + v->object.count = 0; + v->object.capacity = 0; + json_pos++; // skip { + skip_whitespace(); + while (json_input[json_pos] != '}') { + if (v->object.count >= v->object.capacity) { + v->object.capacity = v->object.capacity ? v->object.capacity * 2 : 8; + v->object.pairs = realloc(v->object.pairs, v->object.capacity * sizeof(JsonPair)); + } + JsonPair *p = &v->object.pairs[v->object.count++]; + p->key = parse_string_raw(); + skip_whitespace(); + json_pos++; // skip : + skip_whitespace(); + p->value = parse_value(); + skip_whitespace(); + if (json_input[json_pos] == ',') { json_pos++; skip_whitespace(); } + } + json_pos++; // skip } + return v; +} + +static JsonValue *parse_value(void) { + skip_whitespace(); + char c = json_input[json_pos]; + if (c == '"') return parse_string(); + if (c == '[') return parse_array(); + if (c == '{') return parse_object(); + if (c == 't') { json_pos += 4; JsonValue *v = malloc(sizeof(JsonValue)); v->type = JSON_BOOL; v->bool_val = 1; return v; } + if (c == 'f') { json_pos += 5; JsonValue *v = malloc(sizeof(JsonValue)); v->type = JSON_BOOL; v->bool_val = 0; return v; } + if (c == 'n') { json_pos += 4; JsonValue *v = malloc(sizeof(JsonValue)); v->type = JSON_NULL; return v; } + return parse_number(); +} + +static void json_free(JsonValue *v) { + if (!v) return; + if (v->type == JSON_STRING) free(v->str_val); + else if (v->type == JSON_ARRAY) { + for (int i = 0; i < v->array.count; i++) json_free(v->array.items[i]); + free(v->array.items); + } else if (v->type == JSON_OBJECT) { + for (int i = 0; i < v->object.count; i++) { + free(v->object.pairs[i].key); + json_free(v->object.pairs[i].value); + } + free(v->object.pairs); + } + free(v); +} + +static int json_serialize(JsonValue *v, char *buf, int size) { + int len = 0; + switch (v->type) { + case JSON_NULL: len = snprintf(buf, size, "null"); break; + case JSON_BOOL: len = snprintf(buf, size, v->bool_val ? "true" : "false"); break; + case JSON_NUMBER: len = snprintf(buf, size, "%g", v->num_val); break; + case JSON_STRING: len = snprintf(buf, size, "\"%s\"", v->str_val); break; + case JSON_ARRAY: + len = snprintf(buf, size, "["); + for (int i = 0; i < v->array.count; i++) { + if (i > 0) len += snprintf(buf + len, size - len, ","); + len += json_serialize(v->array.items[i], buf + len, size - len); + } + len += snprintf(buf + len, size - len, "]"); + break; + case JSON_OBJECT: + len = snprintf(buf, size, "{"); + for (int i = 0; i < v->object.count; i++) { + if (i > 0) len += snprintf(buf + len, size - len, ","); + len += snprintf(buf + len, size - len, "\"%s\":", v->object.pairs[i].key); + len += json_serialize(v->object.pairs[i].value, buf + len, size - len); + } + len += snprintf(buf + len, size - len, "}"); + break; + } + return len; +} + +// Test JSON data +static const char *TEST_JSON = + "{\"name\":\"John Doe\",\"age\":30,\"active\":true,\"balance\":1234.56," + "\"address\":{\"street\":\"123 Main St\",\"city\":\"Springfield\",\"zip\":\"12345\"}," + "\"tags\":[\"developer\",\"golang\",\"python\",\"rust\"]," + "\"scores\":[95,87,92,88,91],\"metadata\":null}"; + +int main(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + int n = atoi(argv[1]); + char *output = malloc(4096); + int total_len = 0; + + for (int i = 0; i < n; i++) { + // Parse + json_input = TEST_JSON; + json_pos = 0; + JsonValue *v = parse_value(); + + // Serialize + int len = json_serialize(v, output, 4096); + total_len += len; + + // Free + json_free(v); + } + + printf("%d\n", total_len); + free(output); + return 0; +} diff --git a/benchmarks/json_parse/json_parse.hml b/benchmarks/json_parse/json_parse.hml new file mode 100644 index 0000000..f8526d9 --- /dev/null +++ b/benchmarks/json_parse/json_parse.hml @@ -0,0 +1,35 @@ +// JSON parse/serialize benchmark - Hemlock implementation + +let TEST_JSON = "{\"name\":\"John Doe\",\"age\":30,\"active\":true,\"balance\":1234.56,\"address\":{\"street\":\"123 Main St\",\"city\":\"Springfield\",\"zip\":\"12345\"},\"tags\":[\"developer\",\"golang\",\"python\",\"rust\"],\"scores\":[95,87,92,88,91],\"metadata\":null}"; + +fn parse_int(s) { + let result = 0; + let i = 0; + while (i < s.length) { + let ch = s[i]; + let code: i32 = ch; + let digit = code - 48; + result = result * 10 + digit; + i = i + 1; + } + return result; +} + +fn main() { + let n = parse_int(args[1]); + let total_len = 0; + + let i = 0; + while (i < n) { + // Parse JSON string to object + let data = json_parse(TEST_JSON); + // Serialize back to string + let output = json_stringify(data); + total_len = total_len + output.length; + i = i + 1; + } + + print(total_len); +} + +main(); diff --git a/benchmarks/json_parse/json_parse.js b/benchmarks/json_parse/json_parse.js new file mode 100644 index 0000000..341d55a --- /dev/null +++ b/benchmarks/json_parse/json_parse.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node +// JSON parse/serialize benchmark - JavaScript implementation + +const TEST_JSON = `{ + "name": "John Doe", + "age": 30, + "active": true, + "balance": 1234.56, + "address": { + "street": "123 Main St", + "city": "Springfield", + "zip": "12345" + }, + "tags": ["developer", "golang", "python", "rust"], + "scores": [95, 87, 92, 88, 91], + "metadata": null +}`; + +const n = parseInt(process.argv[2]); + +if (!n) { + console.error('Usage: node json_parse.js '); + process.exit(1); +} + +let totalLen = 0; + +for (let i = 0; i < n; i++) { + // Parse + const data = JSON.parse(TEST_JSON); + // Serialize + const output = JSON.stringify(data); + totalLen += output.length; +} + +console.log(totalLen); diff --git a/benchmarks/json_parse/json_parse.py b/benchmarks/json_parse/json_parse.py new file mode 100644 index 0000000..9ecca67 --- /dev/null +++ b/benchmarks/json_parse/json_parse.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# JSON parse/serialize benchmark - Python implementation + +import sys +import json + +TEST_JSON = '''{ + "name": "John Doe", + "age": 30, + "active": true, + "balance": 1234.56, + "address": { + "street": "123 Main St", + "city": "Springfield", + "zip": "12345" + }, + "tags": ["developer", "golang", "python", "rust"], + "scores": [95, 87, 92, 88, 91], + "metadata": null +}''' + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + n = int(sys.argv[1]) + total_len = 0 + + for _ in range(n): + # Parse + data = json.loads(TEST_JSON) + # Serialize + output = json.dumps(data) + total_len += len(output) + + print(total_len) + +if __name__ == '__main__': + main() diff --git a/benchmarks/json_parse/json_parse.rb b/benchmarks/json_parse/json_parse.rb new file mode 100644 index 0000000..ad604ff --- /dev/null +++ b/benchmarks/json_parse/json_parse.rb @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby +# JSON parse/serialize benchmark - Ruby implementation + +require 'json' + +TEST_JSON = <<~JSON +{ + "name": "John Doe", + "age": 30, + "active": true, + "balance": 1234.56, + "address": { + "street": "123 Main St", + "city": "Springfield", + "zip": "12345" + }, + "tags": ["developer", "golang", "python", "rust"], + "scores": [95, 87, 92, 88, 91], + "metadata": null +} +JSON + +n = ARGV[0]&.to_i + +if n.nil? || n == 0 + STDERR.puts "Usage: ruby json_parse.rb " + exit 1 +end + +total_len = 0 + +n.times do + # Parse + data = JSON.parse(TEST_JSON) + # Serialize + output = JSON.generate(data) + total_len += output.length +end + +puts total_len diff --git a/run.sh b/run.sh index 33429a0..6c08cdf 100755 --- a/run.sh +++ b/run.sh @@ -28,7 +28,7 @@ usage() { echo " --iter N Number of iterations (default: 3)" echo " --help, -h Show this help" echo "" - echo "Benchmarks: fib, array_sum, string_concat, primes_sieve, http_throughput" + echo "Benchmarks: fib, array_sum, string_concat, primes_sieve, json_parse, http_throughput" echo " (leave empty to run all)" } @@ -72,6 +72,9 @@ get_input() { primes_sieve) [[ $QUICK_MODE -eq 1 ]] && echo 100000 || echo 1000000 ;; + json_parse) + [[ $QUICK_MODE -eq 1 ]] && echo 10000 || echo 100000 + ;; http_throughput) # Number of requests (keep low due to connection overhead) [[ $QUICK_MODE -eq 1 ]] && echo 500 || echo 2000 @@ -319,7 +322,7 @@ run_all() { if [[ -n "$BENCHMARK" ]]; then benchmarks="$BENCHMARK" else - benchmarks="fib array_sum string_concat primes_sieve http_throughput" + benchmarks="fib array_sum string_concat primes_sieve json_parse http_throughput" fi local languages="c hemlockc hemlock python javascript ruby"