EventReader.next(), both initializers, and the Event alias are public (an internal typealias in a split file had been shadowing the public Parsing.Event); a new PureXMLPublicAPITests target compiles without testable import so facade gaps fail CI. The large-corpus benchmarks exposed document-order sorting as quadratic on flat fan-outs; the fix's first attempt was itself worse (an eager sibling table built per predicate evaluation), caught by sampling the live run, and replaced with a lazy table: single-node sets never touch order keys and a parent's table builds on its second lookup. XPath at 42 MB drops 27.1 s to 1.57 s (190x to 11.7x); at 210 MB PureXML completes in 9.4 s the query libxml2 refuses at its 10M node-set cap (#143 tracks our configurable budget). Benchmark harness: size series to 210 MB, CORPUS mode for real files (NASA, SwissProt measured), SAX stage, refusal-aware table, NULL-safe C driver. All conformance suites, Linux, and WASM verified. Closes #142.
95 lines
3.0 KiB
C
95 lines
3.0 KiB
C
/* libxml2 side of the PureXML benchmark (scripts/benchmark.sh).
|
|
* Times the same operations the Swift driver times, over the same file,
|
|
* with internal timing so process startup never taints a number.
|
|
* Output: CSV lines "library,operation,bytes,seconds".
|
|
*/
|
|
#include <libxml/parser.h>
|
|
#include <libxml/tree.h>
|
|
#include <libxml/xpath.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/time.h>
|
|
|
|
static double now(void) {
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
return (double)tv.tv_sec + (double)tv.tv_usec / 1e6;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc < 3) {
|
|
fprintf(stderr, "usage: bench-libxml2 <file.xml> <iterations>\n");
|
|
return 1;
|
|
}
|
|
const char *path = argv[1];
|
|
int iterations = atoi(argv[2]);
|
|
xmlInitParser();
|
|
|
|
/* Read the bytes once so parse timing excludes IO. */
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) { perror("open"); return 1; }
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
char *buffer = malloc((size_t)size);
|
|
fread(buffer, 1, (size_t)size, f);
|
|
fclose(f);
|
|
|
|
/* Parse. */
|
|
double best_parse = 1e9;
|
|
xmlDocPtr doc = NULL;
|
|
for (int i = 0; i < iterations; i++) {
|
|
if (doc) xmlFreeDoc(doc);
|
|
double t0 = now();
|
|
doc = xmlReadMemory(buffer, (int)size, path, NULL, XML_PARSE_NONET);
|
|
double dt = now() - t0;
|
|
if (dt < best_parse) best_parse = dt;
|
|
}
|
|
if (!doc) { fprintf(stderr, "parse failed\n"); return 1; }
|
|
printf("libxml2,parse,%ld,%.6f\n", size, best_parse);
|
|
|
|
/* Serialize. */
|
|
double best_serialize = 1e9;
|
|
for (int i = 0; i < iterations; i++) {
|
|
xmlChar *out = NULL;
|
|
int outLen = 0;
|
|
double t0 = now();
|
|
xmlDocDumpMemory(doc, &out, &outLen);
|
|
double dt = now() - t0;
|
|
if (dt < best_serialize) best_serialize = dt;
|
|
xmlFree(out);
|
|
}
|
|
printf("libxml2,serialize,%ld,%.6f\n", size, best_serialize);
|
|
|
|
/* XPath: count a broad selection. libxml2 caps node sets at
|
|
* XPATH_MAX_NODESET_LENGTH (10M): past that the evaluation returns
|
|
* NULL, which is reported rather than dereferenced. */
|
|
double best_xpath = 1e9;
|
|
long count = -1;
|
|
for (int i = 0; i < iterations; i++) {
|
|
double t0 = now();
|
|
xmlXPathContextPtr ctx = xmlXPathNewContext(doc);
|
|
xmlXPathObjectPtr res = xmlXPathEvalExpression((const xmlChar *)"count(//item[@kind='even'])", ctx);
|
|
if (res) {
|
|
count = (long)res->floatval;
|
|
double dt = now() - t0;
|
|
if (dt < best_xpath) best_xpath = dt;
|
|
xmlXPathFreeObject(res);
|
|
}
|
|
xmlXPathFreeContext(ctx);
|
|
if (!res) break;
|
|
}
|
|
if (count >= 0) {
|
|
printf("libxml2,xpath,%ld,%.6f\n", size, best_xpath);
|
|
fprintf(stderr, "libxml2 xpath count: %ld\n", count);
|
|
} else {
|
|
printf("libxml2,xpath,%ld,refused\n", size);
|
|
fprintf(stderr, "libxml2 xpath: evaluation refused (node-set cap)\n");
|
|
}
|
|
|
|
xmlFreeDoc(doc);
|
|
xmlCleanupParser();
|
|
free(buffer);
|
|
return 0;
|
|
}
|