From eccd0267825bb4045c0a93f66c8e8f307b16c183 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 3 Nov 2026 01:27:36 +0100 Subject: [PATCH] perf(resolver): read a Dart barrel's exports as a walk reaches them Every file that imports a barrel read the barrel's whole export list again, though one name's walk stops at the chain's bound of 128 libraries. With the barrel listing the project, that is files x exports: 16,000 libraries took 39.7 s in resolve. Exports are now read when a walk first reaches them, and a walk stops once the bound is passed, since no further library can then be visited or change its answer. So `dart_export_graph_not_walked` is still what a name past the bound gets. Left as it was: an export that a `show` or `hide` rules out costs a filter check per name and does not count toward the bound. complexity_dart_barrel_exports_are_near_linear: 3.71 on the old read, 1.94 now. Signed-off-by: Martin Vogel --- internal/cbm/resolve/adapter_dart.c | 91 +++++++++++++++++++++-------- tests/test_complexity.c | 40 +++++++++++++ 2 files changed, 106 insertions(+), 25 deletions(-) diff --git a/internal/cbm/resolve/adapter_dart.c b/internal/cbm/resolve/adapter_dart.c index c534e4145..5b592eae6 100644 --- a/internal/cbm/resolve/adapter_dart.c +++ b/internal/cbm/resolve/adapter_dart.c @@ -32,6 +32,7 @@ #include "source_adapter_internal.h" #include #include +#include "stats.h" #include "syntax_index.h" #define DART_LIBRARY_RECORD "&library" @@ -52,9 +53,13 @@ typedef struct { bool conditional; /* its library depends on the build's configuration */ } dart_directive_t; +/* A barrel's exports can list the whole project, and every file that imports it would read them + * all again while a name's walk stops at the chain's bound. So they are read as a walk reaches + * them: export_next is the first declaration not read yet. */ typedef struct { dart_directive_t *imports, *exports; - size_t import_count, export_count; + size_t import_count, export_count, export_capacity; + const cbm_source_decl_t *export_next; } dart_library_t; typedef struct dart_local { @@ -636,6 +641,30 @@ static bool dart_filter_passes(const char *show, const char *hide, const char *n return !dart_list_has(hide, name); } +/* False for a directive this table holds in another form, which says nothing. */ +static bool dart_read_directive(dart_ctx_t *ctx, const cbm_source_decl_t *d, + dart_directive_t *out) { + cbm_resolve_stats_add(CBM_RESOLVE_STAT_LOOKUP_PROBES, 1); + char *copy = d->type ? cbm_arena_strdup(ctx->walk->arena, d->type) : NULL; + char *parts[4] = {0}; + int used = 0; + for (char *p = copy; p && used < 4;) { + parts[used++] = p; + char *mark = strchr(p, DART_DIRECTIVE_SEPARATOR); + if (mark) + *mark = '\0'; + p = mark ? mark + 1 : NULL; + } + if (used != 4) + return false; + const char *path = dart_uri_library(ctx->walk, d->file, parts[0]); + if (path && !cbm_source_lookup(ctx->walk, path, DART_LIBRARY_RECORD)) + path = NULL; + *out = + (dart_directive_t){path, parts[1][0] ? parts[1] : NULL, parts[2], parts[3], !parts[0][0]}; + return true; +} + static void dart_read_directives(dart_ctx_t *ctx, const char *library, const char *key, dart_directive_t **out, size_t *out_count) { size_t count = 0; @@ -647,28 +676,36 @@ static void dart_read_directives(dart_ctx_t *ctx, const char *library, const cha *out_count = 0; if (!items) return; - for (const cbm_source_decl_t *d = cbm_source_lookup(ctx->walk, library, key); d; d = d->next) { - char *copy = d->type ? cbm_arena_strdup(ctx->walk->arena, d->type) : NULL; - char *parts[4] = {0}; - int used = 0; - for (char *p = copy; p && used < 4;) { - parts[used++] = p; - char *mark = strchr(p, DART_DIRECTIVE_SEPARATOR); - if (mark) - *mark = '\0'; - p = mark ? mark + 1 : NULL; - } - if (used != 4) - continue; - const char *path = dart_uri_library(ctx->walk, d->file, parts[0]); - if (path && !cbm_source_lookup(ctx->walk, path, DART_LIBRARY_RECORD)) - path = NULL; - items[(*out_count)++] = (dart_directive_t){path, parts[1][0] ? parts[1] : NULL, parts[2], - parts[3], !parts[0][0]}; - } + for (const cbm_source_decl_t *d = cbm_source_lookup(ctx->walk, library, key); d; d = d->next) + if (dart_read_directive(ctx, d, &items[*out_count])) + (*out_count)++; } -static const dart_library_t *dart_library(dart_ctx_t *ctx, const char *path) { +/* The export at this position, read on the way if it has not been; NULL past the last. */ +static const dart_directive_t *dart_export_at(dart_ctx_t *ctx, dart_library_t *library, + size_t position) { + while (position >= library->export_count && library->export_next) { + if (library->export_count == library->export_capacity) { + size_t capacity = library->export_capacity ? library->export_capacity * 2 : 16; + dart_directive_t *items = cbm_arena_alloc(ctx->walk->arena, capacity * sizeof(*items)); + if (!items) { + cbm_source_fail(ctx->walk, "dart_library_allocation"); + return NULL; + } + if (library->export_count) + memcpy(items, library->exports, library->export_count * sizeof(*items)); + library->exports = items; + library->export_capacity = capacity; + } + const cbm_source_decl_t *d = library->export_next; + library->export_next = d->next; + if (dart_read_directive(ctx, d, &library->exports[library->export_count])) + library->export_count++; + } + return position < library->export_count ? &library->exports[position] : NULL; +} + +static dart_library_t *dart_library(dart_ctx_t *ctx, const char *path) { dart_library_t *library = cbm_ht_get(ctx->libraries, path); if (library) return library; @@ -679,7 +716,7 @@ static const dart_library_t *dart_library(dart_ctx_t *ctx, const char *path) { return NULL; } dart_read_directives(ctx, path, DART_IMPORTS, &library->imports, &library->import_count); - dart_read_directives(ctx, path, DART_EXPORTS, &library->exports, &library->export_count); + library->export_next = cbm_source_lookup(ctx->walk, path, DART_EXPORTS); cbm_ht_set(ctx->libraries, key, library); return library; } @@ -693,12 +730,16 @@ static const cbm_source_decl_t *dart_library_public(dart_ctx_t *ctx, const char const cbm_source_decl_t *own = cbm_source_lookup(ctx->walk, path, key); if (own) return own; - const dart_library_t *library = dart_library(ctx, path); + dart_library_t *library = dart_library(ctx, path); if (!library) return NULL; const cbm_source_decl_t *found = NULL; - for (size_t i = 0; i < library->export_count; i++) { - const dart_directive_t *directive = &library->exports[i]; + /* Past the chain's bound no further library can be visited, so none can change this. */ + for (size_t i = 0; !chain->overflow; i++) { + const dart_directive_t *directive = dart_export_at(ctx, library, i); + if (!directive) + break; + cbm_resolve_stats_add(CBM_RESOLVE_STAT_LOOKUP_PROBES, 1); if (!dart_filter_passes(directive->show, directive->hide, name)) continue; const cbm_source_decl_t *exported = diff --git a/tests/test_complexity.c b/tests/test_complexity.c index f38fc4d3d..d9a86def6 100644 --- a/tests/test_complexity.c +++ b/tests/test_complexity.c @@ -3263,6 +3263,45 @@ TEST(complexity_source_same_named_definitions_are_near_linear) { PASS(); } +/* A barrel library re-exports the project, and every library that imports it read its whole + * export list again. */ +static uint64_t cx_dart_barrel_work(int libraries) { + CxSourceFile *files = calloc((size_t)libraries + 1, sizeof(*files)); + if (!files) + return 0; + size_t room = (size_t)libraries * 32 + 1; + char *barrel = malloc(room); + size_t used = 0; + bool ok = barrel != NULL; + for (int i = 0; ok && i < libraries; i++) { + used += (size_t)snprintf(barrel + used, room - used, "export 'a%d.dart';\n", i); + snprintf(files[i].path, sizeof(files[i].path), "lib/a%d.dart", i); + files[i].text = malloc(96); + ok = files[i].text != NULL; + if (ok) + snprintf(files[i].text, 96, "import 'all.dart';\nint f%d() => f%d();\n", i, + (i + 1) % libraries); + } + snprintf(files[libraries].path, sizeof(files[libraries].path), "lib/all.dart"); + files[libraries].text = barrel; + uint64_t work = ok ? cx_source_work(CBM_LANG_DART, files, libraries + 1) : 0; + for (int i = 0; i <= libraries; i++) + free(files[i].text); + free(files); + return work; +} +TEST(complexity_dart_barrel_exports_are_near_linear) { + enum { BARREL_LIBRARIES = 500 }; + uint64_t first = cx_dart_barrel_work(BARREL_LIBRARIES); + uint64_t second = cx_dart_barrel_work(BARREL_LIBRARIES * 2); + double ratio = cx_ratio((double)second, (double)first); + printf(" barrel source probes %llu -> %llu (ratio %.2f)\n", (unsigned long long)first, + (unsigned long long)second, ratio); + ASSERT_GTE(first, (uint64_t)BARREL_LIBRARIES); + ASSERT_TRUE(ratio >= CX_RATIO_LO && ratio <= CX_RATIO_HI); + PASS(); +} + /* A function of route registrations asks, for each, what its receiver was bound to. */ CX_LINE(cx_line_go_route, " r.GET(\"/p%d\", h)\n") CX_LINE3(cx_line_go_group, " g%d := r.Group(\"/g%d\")\n g%d.GET(\"/x\", h)\n") @@ -3418,6 +3457,7 @@ SUITE(complexity) { RUN_TEST(complexity_syntax_index_prev_siblings_answer_as_tree_sitter_does); RUN_TEST(complexity_prev_sibling_walk_is_linear); RUN_TEST(complexity_source_same_named_definitions_are_near_linear); + RUN_TEST(complexity_dart_barrel_exports_are_near_linear); RUN_TEST(complexity_go_route_bindings_are_near_linear); RUN_TEST(complexity_flat_preceding_label_is_linear); RUN_TEST(complexity_import_map_is_linear_in_a_files_imports);