libs/html-threaded/src/html_gen.c

347 lines

1/*
2 * html_gen.c - Parallel HTML file writer using pthreads
3 *
4 * Implements a thread pool for parallel file writing. Each worker thread
5 * grabs files from a shared queue using atomic operations for load balancing.
6 */
7
8#define _GNU_SOURCE
9#include <stdio.h>
10#include <stdlib.h>
11#include <string.h>
12#include <pthread.h>
13#include <stdatomic.h>
14#include <unistd.h>
15#include <sys/time.h>
16#include <errno.h>
17#include <sys/stat.h>
18#include <libgen.h>
19
20#include "html_gen.h"
21
22/* {{{ Constants */
23#define INITIAL_CAPACITY 1024
24#define MAX_PATH_LEN 4096
25/* }}} */
26
27/* {{{ File entry structure */
28typedef struct {
29 char* filepath;
30 char* content;
31 size_t content_len;
32} FileEntry;
33/* }}} */
34
35/* {{{ Context structure */
36struct HtmlGenContext {
37 /* Thread pool */
38 int num_threads;
39 pthread_t* threads;
40 int threads_running;
41
42 /* File queue */
43 FileEntry* files;
44 uint32_t file_count;
45 uint32_t file_capacity;
46
47 /* Work distribution (atomic for lock-free access) */
48 atomic_uint work_index;
49 atomic_uint files_written;
50 atomic_uint files_failed;
51 atomic_ulong bytes_written;
52
53 /* Timing */
54 struct timeval start_time;
55 struct timeval end_time;
56
57 /* State */
58 int is_running;
59};
60/* }}} */
61
62/* {{{ Forward declarations */
63static void* worker_thread(void* arg);
64static int ensure_directory(const char* filepath);
65/* }}} */
66
67/* {{{ htmlgen_init */
68HtmlGenContext* htmlgen_init(int num_threads) {
69 HtmlGenContext* ctx = calloc(1, sizeof(HtmlGenContext));
70 if (!ctx) return NULL;
71
72 /* Auto-detect CPU cores if num_threads is 0 */
73 if (num_threads <= 0) {
74 num_threads = sysconf(_SC_NPROCESSORS_ONLN);
75 if (num_threads <= 0) num_threads = 4;
76 }
77
78 ctx->num_threads = num_threads;
79 ctx->threads = calloc(num_threads, sizeof(pthread_t));
80 if (!ctx->threads) {
81 free(ctx);
82 return NULL;
83 }
84
85 /* Initialize file queue */
86 ctx->file_capacity = INITIAL_CAPACITY;
87 ctx->files = calloc(ctx->file_capacity, sizeof(FileEntry));
88 if (!ctx->files) {
89 free(ctx->threads);
90 free(ctx);
91 return NULL;
92 }
93
94 ctx->file_count = 0;
95 ctx->threads_running = 0;
96 ctx->is_running = 0;
97
98 /* Initialize atomics */
99 atomic_init(&ctx->work_index, 0);
100 atomic_init(&ctx->files_written, 0);
101 atomic_init(&ctx->files_failed, 0);
102 atomic_init(&ctx->bytes_written, 0);
103
104 return ctx;
105}
106/* }}} */
107
108/* {{{ htmlgen_add_file */
109HtmlGenResult htmlgen_add_file(HtmlGenContext* ctx,
110 const char* filepath,
111 const char* content,
112 size_t content_len) {
113 if (!ctx) return HTMLGEN_ERROR_INVALID_CONTEXT;
114 if (ctx->is_running) return HTMLGEN_ERROR_ALREADY_RUNNING;
115
116 /* Grow array if needed */
117 if (ctx->file_count >= ctx->file_capacity) {
118 uint32_t new_capacity = ctx->file_capacity * 2;
119 FileEntry* new_files = realloc(ctx->files, new_capacity * sizeof(FileEntry));
120 if (!new_files) return HTMLGEN_ERROR_OUT_OF_MEMORY;
121 ctx->files = new_files;
122 ctx->file_capacity = new_capacity;
123 }
124
125 /* Copy filepath */
126 FileEntry* entry = &ctx->files[ctx->file_count];
127 entry->filepath = strdup(filepath);
128 if (!entry->filepath) return HTMLGEN_ERROR_OUT_OF_MEMORY;
129
130 /* Copy content */
131 entry->content = malloc(content_len + 1);
132 if (!entry->content) {
133 free(entry->filepath);
134 return HTMLGEN_ERROR_OUT_OF_MEMORY;
135 }
136 memcpy(entry->content, content, content_len);
137 entry->content[content_len] = '\0';
138 entry->content_len = content_len;
139
140 ctx->file_count++;
141 return HTMLGEN_SUCCESS;
142}
143/* }}} */
144
145/* {{{ ensure_directory - Create parent directories if needed */
146static int ensure_directory(const char* filepath) {
147 char* path_copy = strdup(filepath);
148 if (!path_copy) return -1;
149
150 char* dir = dirname(path_copy);
151
152 /* Check if directory exists */
153 struct stat st;
154 if (stat(dir, &st) == 0 && S_ISDIR(st.st_mode)) {
155 free(path_copy);
156 return 0;
157 }
158
159 /* Create directory (and parents) */
160 char cmd[MAX_PATH_LEN + 16];
161 snprintf(cmd, sizeof(cmd), "mkdir -p '%s'", dir);
162 int ret = system(cmd);
163
164 free(path_copy);
165 return ret;
166}
167/* }}} */
168
169/* {{{ worker_thread - Thread function for parallel file writing */
170static void* worker_thread(void* arg) {
171 HtmlGenContext* ctx = (HtmlGenContext*)arg;
172
173 while (1) {
174 /* Atomically grab next work item */
175 uint32_t index = atomic_fetch_add(&ctx->work_index, 1);
176
177 /* Check if we're done */
178 if (index >= ctx->file_count) {
179 break;
180 }
181
182 FileEntry* entry = &ctx->files[index];
183
184 /* Ensure parent directory exists */
185 if (ensure_directory(entry->filepath) != 0) {
186 atomic_fetch_add(&ctx->files_failed, 1);
187 continue;
188 }
189
190 /* Write file */
191 FILE* f = fopen(entry->filepath, "w");
192 if (!f) {
193 atomic_fetch_add(&ctx->files_failed, 1);
194 continue;
195 }
196
197 size_t written = fwrite(entry->content, 1, entry->content_len, f);
198 fclose(f);
199
200 if (written == entry->content_len) {
201 atomic_fetch_add(&ctx->files_written, 1);
202 atomic_fetch_add(&ctx->bytes_written, written);
203 } else {
204 atomic_fetch_add(&ctx->files_failed, 1);
205 }
206 }
207
208 return NULL;
209}
210/* }}} */
211
212/* {{{ htmlgen_write_all */
213HtmlGenResult htmlgen_write_all(HtmlGenContext* ctx) {
214 if (!ctx) return HTMLGEN_ERROR_INVALID_CONTEXT;
215 if (ctx->is_running) return HTMLGEN_ERROR_ALREADY_RUNNING;
216 if (ctx->file_count == 0) return HTMLGEN_SUCCESS;
217
218 ctx->is_running = 1;
219
220 /* Reset counters */
221 atomic_store(&ctx->work_index, 0);
222 atomic_store(&ctx->files_written, 0);
223 atomic_store(&ctx->files_failed, 0);
224 atomic_store(&ctx->bytes_written, 0);
225
226 /* Record start time */
227 gettimeofday(&ctx->start_time, NULL);
228
229 /* Spawn worker threads */
230 for (int i = 0; i < ctx->num_threads; i++) {
231 if (pthread_create(&ctx->threads[i], NULL, worker_thread, ctx) != 0) {
232 /* Thread creation failed - wait for already-started threads */
233 for (int j = 0; j < i; j++) {
234 pthread_join(ctx->threads[j], NULL);
235 }
236 ctx->is_running = 0;
237 return HTMLGEN_ERROR_INIT_FAILED;
238 }
239 }
240 ctx->threads_running = ctx->num_threads;
241
242 /* Wait for all threads to complete */
243 for (int i = 0; i < ctx->num_threads; i++) {
244 pthread_join(ctx->threads[i], NULL);
245 }
246
247 /* Record end time */
248 gettimeofday(&ctx->end_time, NULL);
249
250 ctx->threads_running = 0;
251 ctx->is_running = 0;
252
253 /* Check for failures */
254 if (atomic_load(&ctx->files_failed) > 0) {
255 return HTMLGEN_ERROR_WRITE_FAILED;
256 }
257
258 return HTMLGEN_SUCCESS;
259}
260/* }}} */
261
262/* {{{ htmlgen_get_progress */
263float htmlgen_get_progress(HtmlGenContext* ctx) {
264 if (!ctx || ctx->file_count == 0) return 0.0f;
265
266 uint32_t completed = atomic_load(&ctx->files_written) +
267 atomic_load(&ctx->files_failed);
268 return (float)completed / (float)ctx->file_count;
269}
270/* }}} */
271
272/* {{{ htmlgen_get_stats */
273HtmlGenResult htmlgen_get_stats(HtmlGenContext* ctx, HtmlGenStats* stats) {
274 if (!ctx) return HTMLGEN_ERROR_INVALID_CONTEXT;
275 if (!stats) return HTMLGEN_ERROR_INVALID_CONTEXT;
276
277 stats->total_files = ctx->file_count;
278 stats->files_written = atomic_load(&ctx->files_written);
279 stats->files_failed = atomic_load(&ctx->files_failed);
280 stats->total_bytes = atomic_load(&ctx->bytes_written);
281
282 /* Calculate elapsed time */
283 double start = ctx->start_time.tv_sec + ctx->start_time.tv_usec / 1000000.0;
284 double end = ctx->end_time.tv_sec + ctx->end_time.tv_usec / 1000000.0;
285 stats->elapsed_seconds = end - start;
286
287 return HTMLGEN_SUCCESS;
288}
289/* }}} */
290
291/* {{{ htmlgen_clear */
292void htmlgen_clear(HtmlGenContext* ctx) {
293 if (!ctx) return;
294
295 /* Free all file entries */
296 for (uint32_t i = 0; i < ctx->file_count; i++) {
297 free(ctx->files[i].filepath);
298 free(ctx->files[i].content);
299 }
300
301 ctx->file_count = 0;
302
303 /* Reset counters */
304 atomic_store(&ctx->work_index, 0);
305 atomic_store(&ctx->files_written, 0);
306 atomic_store(&ctx->files_failed, 0);
307 atomic_store(&ctx->bytes_written, 0);
308}
309/* }}} */
310
311/* {{{ htmlgen_destroy */
312void htmlgen_destroy(HtmlGenContext* ctx) {
313 if (!ctx) return;
314
315 /* Free all file entries */
316 for (uint32_t i = 0; i < ctx->file_count; i++) {
317 free(ctx->files[i].filepath);
318 free(ctx->files[i].content);
319 }
320
321 free(ctx->files);
322 free(ctx->threads);
323 free(ctx);
324}
325/* }}} */
326
327/* {{{ htmlgen_error_string */
328const char* htmlgen_error_string(HtmlGenResult result) {
329 switch (result) {
330 case HTMLGEN_SUCCESS:
331 return "Success";
332 case HTMLGEN_ERROR_INIT_FAILED:
333 return "Initialization failed";
334 case HTMLGEN_ERROR_OUT_OF_MEMORY:
335 return "Out of memory";
336 case HTMLGEN_ERROR_WRITE_FAILED:
337 return "File write failed";
338 case HTMLGEN_ERROR_INVALID_CONTEXT:
339 return "Invalid context";
340 case HTMLGEN_ERROR_ALREADY_RUNNING:
341 return "Already running";
342 default:
343 return "Unknown error";
344 }
345}
346/* }}} */
347