From f4f9442cfd15a76aafcee52031ae9ec878e422e7 Mon Sep 17 00:00:00 2001 From: Yggdrasil75 Date: Thu, 6 Nov 2025 09:56:57 -0500 Subject: [PATCH] much better. --- .gitignore | 3 +- main.cpp | 58 ++++++++++++++++++++++- util/jxlwriter.hpp | 35 ++++---------- util/simple_httpserver.hpp | 97 +++++++++++++++++++++++++++++--------- util/timing_decorator.hpp | 2 +- web/index.html | 16 ++++++- web/script.js | 72 +++++++++++++++++++++++++++- 7 files changed, 230 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index 4bb2d65..1a7ad3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /output/ -/bin/ \ No newline at end of file +/bin/ +/web/output/ \ No newline at end of file diff --git a/main.cpp b/main.cpp index 3abe868..8a5dccd 100644 --- a/main.cpp +++ b/main.cpp @@ -8,6 +8,8 @@ // Function to convert hex color string to Vec4 Vec4 hexToVec4(const std::string& hex) { + TIME_FUNCTION; + if (hex.length() != 6) { return Vec4(0, 0, 0, 1); // Default to black if invalid } @@ -20,7 +22,8 @@ Vec4 hexToVec4(const std::string& hex) { // Generate gradient image bool generateGradientImage(const std::string& filename, int width = 512, int height = 512) { - #TIME_FUNCTION; + TIME_FUNCTION; + const int POINTS_PER_DIM = 256; Grid2 grid; @@ -67,6 +70,39 @@ bool generateGradientImage(const std::string& filename, int width = 512, int hei return JXLWriter::saveJXL(filename, imageData, width, height); } +// Add this function to get timing stats as JSON +std::string getTimingStatsJSON() { + + auto stats = FunctionTimer::getStats(); + std::stringstream json; + + json << "["; + bool first = true; + + for (const auto& [func_name, data] : stats) { + if (!first) json << ","; + first = false; + + auto percentiles = FunctionTimer::calculatePercentiles(data.timings); + + json << "{" + << "\"function\":\"" << func_name << "\"," + << "\"call_count\":" << data.call_count << "," + << "\"total_time\":" << std::fixed << std::setprecision(6) << data.total_time << "," + << "\"avg_time\":" << std::fixed << std::setprecision(6) << data.avg_time() << "," + << "\"min_time\":" << std::fixed << std::setprecision(6) << percentiles.min << "," + << "\"max_time\":" << std::fixed << std::setprecision(6) << percentiles.max << "," + << "\"median_time\":" << std::fixed << std::setprecision(6) << percentiles.median << "," + << "\"p90_time\":" << std::fixed << std::setprecision(6) << percentiles.p90 << "," + << "\"p95_time\":" << std::fixed << std::setprecision(6) << percentiles.p95 << "," + << "\"p99_time\":" << std::fixed << std::setprecision(6) << percentiles.p99 + << "}"; + } + + json << "]"; + return json.str(); +} + int main(int argc, char* argv[]) { // Check command line arguments int port = 8080; @@ -94,7 +130,7 @@ int main(int argc, char* argv[]) { // Generate gradient image before starting server std::cout << "Generating gradient image..." << std::endl; - if (generateGradientImage(webRoot + "/gradient.jxl")) { + if (generateGradientImage(webRoot + "/output/gradient.jxl")) { std::cout << "Gradient image generated successfully" << std::endl; } else { std::cerr << "Failed to generate gradient image" << std::endl; @@ -103,6 +139,23 @@ int main(int argc, char* argv[]) { SimpleHTTPServer server(port, webRoot); + // Add timing stats endpoint + server.addRoute("/api/timing-stats", [](const std::string& method, const std::string& body) { + if (method == "GET") { + return std::make_pair(200, getTimingStatsJSON()); + } + //return std::make_pair(405, "{\"error\":\"Method Not Allowed\"}"); + }); + + // Add clear stats endpoint + server.addRoute("/api/clear-stats", [](const std::string& method, const std::string& body) { + if (method == "POST") { + FunctionTimer::clearStats(); + return std::make_pair(200, "{\"status\":\"success\"}"); + } + return std::make_pair(405, "{\"error\":\"Method Not Allowed\"}"); + }); + if (!server.start()) { std::cerr << "Failed to start server on port " << port << std::endl; return 1; @@ -110,6 +163,7 @@ int main(int argc, char* argv[]) { std::cout << "Server running on http://localhost:" << port << std::endl; std::cout << "Web root: " << webRoot << std::endl; + std::cout << "Timing stats available at /api/timing-stats" << std::endl; std::cout << "Press Ctrl+C to stop the server" << std::endl; server.handleRequests(); diff --git a/util/jxlwriter.hpp b/util/jxlwriter.hpp index 8967de6..ffdbe3b 100644 --- a/util/jxlwriter.hpp +++ b/util/jxlwriter.hpp @@ -8,9 +8,8 @@ #include #include #include "vec3.hpp" -// #include "../jxl/encode.h" +#include "timing_decorator.hpp" #include -// #include "../jxl/thread_parallel_runner.h" #include class JXLWriter { @@ -64,10 +63,8 @@ private: public: // Save a 2D vector of Vec3 (RGB) colors as JXL // Vec3 components: x = red, y = green, z = blue (values in range [0,1]) - static bool saveJXL(const std::string& filename, - const std::vector>& pixels, - float quality = 90.0f, // Quality setting (0-100) - int effort = 7) { // Encoding effort (3-9, higher = slower but better compression) + static bool saveJXL(const std::string& filename, const std::vector>& pixels, + float quality = 90.0f, int effort = 7) { if (pixels.empty() || pixels[0].empty()) { return false; } @@ -85,12 +82,8 @@ public: return saveJXL(filename, pixels, width, height, quality, effort); } - // Alternative interface with width/height and flat vector (row-major order) - static bool saveJXL(const std::string& filename, - const std::vector& pixels, - int width, int height, - float quality = 90.0f, - int effort = 7) { + static bool saveJXL(const std::string& filename, const std::vector& pixels, + int width, int height, float quality = 90.0f, int effort = 7) { if (pixels.size() != width * height) { return false; } @@ -107,11 +100,9 @@ public: } // Save from 1D vector of uint8_t pixels (RGB order: pixels[i]=r, pixels[i+1]=g, pixels[i+2]=b) - static bool saveJXL(const std::string& filename, - const std::vector& pixels, - int width, int height, - float quality = 90.0f, - int effort = 7) { + static bool saveJXL(const std::string& filename, const std::vector& pixels, + int width, int height, float quality = 90.0f, int effort = 7) { + TIME_FUNCTION; if (pixels.size() != width * height * 3) { return false; } @@ -223,20 +214,14 @@ public: } private: - static bool saveJXL(const std::string& filename, - const std::vector>& pixels, - int width, int height, - float quality, - int effort) { - // Create directory if needed + static bool saveJXL(const std::string& filename, const std::vector>& pixels, + int width, int height, float quality, int effort) { if (!createDirectoryIfNeeded(filename)) { return false; } - // Convert Vec3 pixels to interleaved RGB std::vector rgbData = convertToRGB(pixels, width, height); - // Use the existing uint8_t version return saveJXL(filename, rgbData, width, height, quality, effort); } }; diff --git a/util/simple_httpserver.hpp b/util/simple_httpserver.hpp index 1dc87b6..17ddb24 100644 --- a/util/simple_httpserver.hpp +++ b/util/simple_httpserver.hpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include "timing_decorator.hpp" #ifdef _WIN32 #include @@ -28,6 +31,10 @@ private: bool running; std::string webRoot; + // Route handler type + using RouteHandler = std::function(const std::string&, const std::string&)>; + std::unordered_map routes; + // Read file content std::string readFile(const std::string& filename) { std::ifstream file(filename, std::ios::binary); @@ -55,14 +62,17 @@ private: // Send HTTP response void sendResponse(int clientSocket, const std::string& content, const std::string& contentType = "text/html", int statusCode = 200) { + TIME_FUNCTION; std::string statusText = "OK"; if (statusCode == 404) statusText = "Not Found"; if (statusCode == 500) statusText = "Internal Server Error"; + if (statusCode == 405) statusText = "Method Not Allowed"; std::ostringstream response; response << "HTTP/1.1 " << statusCode << " " << statusText << "\r\n"; response << "Content-Type: " << contentType << "\r\n"; response << "Content-Length: " << content.length() << "\r\n"; + response << "Access-Control-Allow-Origin: *\r\n"; response << "Connection: close\r\n"; response << "\r\n"; response << content; @@ -71,18 +81,23 @@ private: send(clientSocket, responseStr.c_str(), responseStr.length(), 0); } + // Extract method and path from HTTP request + std::pair parseRequest(const std::string& request) { + TIME_FUNCTION; + std::istringstream iss(request); + std::string method, path; + iss >> method >> path; + return {method, path}; + } + // Extract file path from HTTP request std::string getFilePath(const std::string& request) { - // Find the start of the path after "GET " - size_t start = request.find("GET ") + 4; - if (start == std::string::npos) return ""; + auto [method, path] = parseRequest(request); - // Find the end of the path (space or ?) - size_t end = request.find(" ", start); - if (end == std::string::npos) end = request.find("?", start); - if (end == std::string::npos) return ""; - - std::string path = request.substr(start, end - start); + // Only handle GET requests for files + if (method != "GET") { + return ""; + } // Default to index.html for root path if (path == "/") { @@ -96,6 +111,31 @@ private: return path; } + + // Check if path is a registered route + bool isRoute(const std::string& path) { + return routes.find(path) != routes.end(); + } + + // Handle route request + void handleRoute(int clientSocket, const std::string& request) { + TIME_FUNCTION; + auto [method, path] = parseRequest(request); + + // Extract request body if present + std::string body; + size_t bodyPos = request.find("\r\n\r\n"); + if (bodyPos != std::string::npos) { + body = request.substr(bodyPos + 4); + } + + if (routes.find(path) != routes.end()) { + auto [statusCode, response] = routes[path](method, body); + sendResponse(clientSocket, response, "application/json", statusCode); + } else { + sendResponse(clientSocket, "404 Not Found", "text/plain", 404); + } + } public: SimpleHTTPServer(int port = 8080, const std::string& webRoot = "web") @@ -105,6 +145,11 @@ public: stop(); } + // Add a route handler + void addRoute(const std::string& path, RouteHandler handler) { + routes[path] = handler; + } + bool start() { #ifdef _WIN32 WSADATA wsaData; @@ -186,21 +231,29 @@ public: if (bytesReceived > 0) { std::string request(buffer); - std::string filePath = getFilePath(request); + auto [method, path] = parseRequest(request); - if (!filePath.empty()) { - std::cout << "Serving: " << filePath << std::endl; - - std::string fullPath = webRoot + "/" + filePath; - std::string content = readFile(fullPath); - - if (!content.empty()) { - sendResponse(clientSocket, content, getContentType(filePath)); - } else { - sendResponse(clientSocket, "404 Not Found: " + filePath, "text/plain", 404); - } + // Check if this is a registered route + if (isRoute(path)) { + handleRoute(clientSocket, request); } else { - sendResponse(clientSocket, "400 Bad Request", "text/plain", 400); + // Handle file serving for GET requests + std::string filePath = getFilePath(request); + + if (!filePath.empty()) { + std::cout << "Serving: " << filePath << std::endl; + + std::string fullPath = webRoot + "/" + filePath; + std::string content = readFile(fullPath); + + if (!content.empty()) { + sendResponse(clientSocket, content, getContentType(filePath)); + } else { + sendResponse(clientSocket, "404 Not Found: " + filePath, "text/plain", 404); + } + } else { + sendResponse(clientSocket, "400 Bad Request", "text/plain", 400); + } } } diff --git a/util/timing_decorator.hpp b/util/timing_decorator.hpp index 218ae86..780aaa0 100644 --- a/util/timing_decorator.hpp +++ b/util/timing_decorator.hpp @@ -47,10 +47,10 @@ public: // Clear all statistics static void clearStats(); + static PercentileStats calculatePercentiles(const std::vector& timings); private: static std::unordered_map stats_; - static PercentileStats calculatePercentiles(const std::vector& timings); }; // Macro to easily time functions - similar to Python decorator diff --git a/web/index.html b/web/index.html index 5bae13a..f5f6bb1 100644 --- a/web/index.html +++ b/web/index.html @@ -11,12 +11,26 @@
+ +
- Dynamic Gradient + Dynamic Gradient
+ + +
+ diff --git a/web/script.js b/web/script.js index aee9cc5..7d2b05f 100644 --- a/web/script.js +++ b/web/script.js @@ -18,7 +18,6 @@ function refreshImage() { }; } - function toggleAutoRefresh() { const button = document.getElementById('autoRefreshBtn'); @@ -36,6 +35,77 @@ function toggleAutoRefresh() { } } +function showStats() { + fetch('/api/timing-stats') + .then(response => response.json()) + .then(data => { + displayStats(data); + document.getElementById('statsPanel').style.display = 'block'; + }) + .catch(error => { + console.error('Error fetching stats:', error); + updateStatus('Error loading stats', 'error'); + }); +} + +function hideStats() { + document.getElementById('statsPanel').style.display = 'none'; +} + +function clearStats() { + fetch('/api/clear-stats', { method: 'POST' }) + .then(response => response.json()) + .then(data => { + updateStatus('Statistics cleared'); + hideStats(); + }) + .catch(error => { + console.error('Error clearing stats:', error); + updateStatus('Error clearing stats', 'error'); + }); +} + +function displayStats(stats) { + const statsContent = document.getElementById('statsContent'); + + if (stats.length === 0) { + statsContent.innerHTML = '

No timing data available.

'; + return; + } + + let html = ''; + html += ''; + + stats.forEach(stat => { + html += ` + + + + + + + + + + + `; + }); + + html += '
FunctionCallsTotal (s)Avg (s)Min (s)Max (s)Median (s)P90 (s)P95 (s)P99 (s)
${stat.function}${stat.call_count}${parseFloat(stat.total_time).toFixed(6)}${parseFloat(stat.avg_time).toFixed(6)}${parseFloat(stat.min_time).toFixed(6)}${parseFloat(stat.max_time).toFixed(6)}${parseFloat(stat.median_time).toFixed(6)}${parseFloat(stat.p90_time).toFixed(6)}${parseFloat(stat.p95_time).toFixed(6)}${parseFloat(stat.p99_time).toFixed(6)}
'; + statsContent.innerHTML = html; +} + +function updateStatus(message, type = 'info') { + const statusEl = document.getElementById('status'); + statusEl.textContent = message; + statusEl.className = `status ${type}`; + + // Auto-hide after 5 seconds + setTimeout(() => { + statusEl.textContent = ''; + statusEl.className = 'status'; + }, 5000); +} // Auto-refresh when page loads document.addEventListener('DOMContentLoaded', function() {