feat(lazy-load): extend manifest to containers/ + skip container scan (Phase 5)

Containers used to be eager-loaded via `sourceScanFiles "containers"`
even under LP_LAZY=1 — sourcing all ~160 installer functions up front.
Phase 5 brings them into the autoload-stub mechanism.

generate_function_manifest.sh now scans BOTH scripts/ AND containers/
(maxdepth 3, matching sourceScanFiles' existing prune), with a per-entry
root selector so stub emission uses the right base directory:

  scripts/peer/peer_add.sh    →  source "${install_scripts_dir}peer/peer_add.sh"
  containers/linkding/linkding.sh →  source "${install_containers_dir}linkding/linkding.sh"

New manifest exports:
  LP_FN_MAP             funcname → relpath        (existing)
  LP_FN_ROOT            funcname → scripts|containers   NEW
  LP_EAGER_FILES        "<root>:<relpath>" entries     NEW format
  ~860 autoload stubs   (was ~700; +160 from containers)

Loader changes (initilize_files.sh):
  - Parses LP_EAGER_FILES entries as `root:path`, dispatches to the
    right install_*_dir. Pre-Phase-5 entries without a colon default to
    scripts (backwards-compatible).
  - sourceScanFiles "containers" is skipped when LP_LAZY=1 AND
    LP_FN_MAP is loaded (manifest-driven autoload covers it).
    Eager mode and lazy-with-missing-manifest both still run the scan.

Measurement target: ~70 ms saved on top of Phase 4. Verified separately
in the commit message of the next deploy.

Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-26 21:30:36 +01:00
parent a27be53eaf
commit 77342c8047
3 changed files with 1320 additions and 59 deletions

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,9 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" && "$1" == "run" ]]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ARRAYS_DIR="$SCRIPT_DIR/arrays"
SCRIPTS_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
SCRIPTS_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" # …/scripts
INSTALL_DIR="$(dirname "$SCRIPTS_DIR")" # …/install (parent of scripts)
CONTAINERS_DIR="$INSTALL_DIR/containers" # sibling of scripts
OUTPUT="$ARRAYS_DIR/function_manifest.sh"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
@ -163,22 +165,45 @@ declare -a eager_files
total_files=0
total_fns=0
# Find all .sh under scripts/ (no symlinks, no hidden).
while IFS= read -r -d '' file; do
rel=$(realpath --relative-to="$SCRIPTS_DIR" "$file")
# fn_to_root parallels fn_to_file: tracks whether the source file lives under
# scripts/ ("scripts") or containers/ ("containers") so the stub emission can
# pick the right ${install_*_dir} prefix. Same for eager_root for LP_EAGER_FILES.
declare -A fn_to_root
declare -A eager_root
# Walk one root. $1 = directory, $2 = root label ("scripts" or "containers"),
# $3 = max depth for find (-maxdepth N). The scan also honours the skip-list
# (paths relative to the root, so scripts/ skip entries don't match container
# files and vice versa).
scan_root() {
local root_dir="$1" root_label="$2" depth="$3"
while IFS= read -r -d '' file; do
local rel
rel=$(realpath --relative-to="$root_dir" "$file")
# Skip-list keys are scoped per root. should_skip() only handles the
# scripts/ ones today; containers/ skip-list is inline here.
if [[ "$root_label" == "scripts" ]]; then
should_skip "$rel" && continue
else
# Container-side skips: anything under a resources/ subdir is data
# not code; the existing sourceScanFiles already prunes those.
case "$rel" in
*/resources/*) continue ;;
esac
fi
total_files=$((total_files + 1))
is_eager=0
local is_eager=0
while IFS= read -r tag; do
case "$tag" in
fn:*)
name="${tag#fn:}"
local name="${tag#fn:}"
if [[ -n "${fn_to_file[$name]:-}" && "${fn_to_file[$name]}" != "$rel" ]]; then
fn_collisions[$name]="${fn_collisions[$name]:-${fn_to_file[$name]}}"$'\t'"$rel"
fi
fn_to_file[$name]="$rel"
fn_to_root[$name]="$root_label"
total_fns=$((total_fns + 1))
;;
eager:)
@ -187,8 +212,19 @@ while IFS= read -r -d '' file; do
esac
done < <(analyze_file "$file")
(( is_eager )) && eager_files+=("$rel")
done < <(find "$SCRIPTS_DIR" -type f -name '*.sh' -print0)
if (( is_eager )); then
eager_files+=("$rel")
eager_root["$rel"]="$root_label"
fi
done < <(find "$root_dir" -maxdepth "$depth" -type f -name '*.sh' -print0)
}
# scripts/ — deep walk, the existing behaviour.
scan_root "$SCRIPTS_DIR" scripts 99
# containers/ — match the sourceScanFiles "containers" maxdepth of 3.
if [[ -d "$CONTAINERS_DIR" ]]; then
scan_root "$CONTAINERS_DIR" containers 3
fi
# Emit the manifest.
{
@ -200,33 +236,48 @@ done < <(find "$SCRIPTS_DIR" -type f -name '*.sh' -print0)
printf '# Function name → relative path. Used by the lazy loader (LP_LAZY=1)\n'
printf '# to install an autoload stub for each public function. First call to a\n'
printf '# stub sources the real file, which redefines the function with the real\n'
printf '# body; subsequent calls hit the real one directly.\n'
printf '# body; subsequent calls hit the real one directly. Path is relative to\n'
printf '# install_scripts_dir for "scripts" entries, install_containers_dir for\n'
printf '# "containers" entries — see LP_FN_ROOT below.\n'
printf 'declare -gA LP_FN_MAP=(\n'
# Sort for stable diff output.
while IFS= read -r name; do
printf ' [%s]="%s"\n' "$name" "${fn_to_file[$name]}"
done < <(printf '%s\n' "${!fn_to_file[@]}" | sort)
printf ')\n\n'
printf '# Per-function root selector — "scripts" or "containers". Mirrors\n'
printf '# LP_FN_MAP; used by the loader / debugging tools.\n'
printf 'declare -gA LP_FN_ROOT=(\n'
while IFS= read -r name; do
printf ' [%s]="%s"\n' "$name" "${fn_to_root[$name]}"
done < <(printf '%s\n' "${!fn_to_root[@]}" | sort)
printf ')\n\n'
printf '# Files with top-level side effects (variable assignments, source calls,\n'
printf '# command invocations outside any function). Lazy mode MUST source these\n'
printf '# Files with top-level side effects. Lazy mode MUST source these\n'
printf '# unconditionally — deferring them would skip the side effect, not just\n'
printf '# defer a function definition.\n'
printf '# defer a function definition. Stored as "<root>:<relpath>" so the\n'
printf '# loader picks the right base dir; existing entries without a prefix\n'
printf '# (pre-Phase-5 manifests) default to scripts/.\n'
printf 'LP_EAGER_FILES=(\n'
while IFS= read -r f; do
printf ' "%s"\n' "$f"
# No `local` here — we're inside a `{ … } > FILE` command group, not
# a function. `local` errors out at this scope in bash.
root="${eager_root[$f]:-scripts}"
printf ' "%s:%s"\n' "$root" "$f"
done < <(printf '%s\n' "${eager_files[@]}" | sort -u)
printf ')\n\n'
printf '# Autoload stubs — one per public function. First call sources the\n'
printf '# real file (which redefines this stub with the real body), then\n'
printf '# re-invokes. Sourced inline instead of eval-in-loop because bash\n'
printf '# parses one large file faster than it evals 700 small snippets.\n'
printf '# Only emitted when the manifest is read; behaviour-neutral when the\n'
printf '# loader does not flip into LP_LAZY=1 mode.\n'
printf '# parses one large file faster than it evals snippets at startup.\n'
while IFS= read -r name; do
printf '%s() { source "${install_scripts_dir}%s"; %s "$@"; }\n' \
"$name" "${fn_to_file[$name]}" "$name"
root="${fn_to_root[$name]}"
case "$root" in
containers) base_var='install_containers_dir' ;;
*) base_var='install_scripts_dir' ;;
esac
printf '%s() { source "${%s}%s"; %s "$@"; }\n' \
"$name" "$base_var" "${fn_to_file[$name]}" "$name"
done < <(printf '%s\n' "${!fn_to_file[@]}" | sort)
} > "$OUTPUT"

View File

@ -70,20 +70,32 @@ sourceInitilize()
else
source "$manifest"
fi
# Eager-source the side-effect files. These define vars or run
# commands at top level; lazy stubs would skip those side effects.
local _eager
# Eager-source the side-effect files. Entries are `<root>:<relpath>`
# where root selects the base dir ("scripts" → install_scripts_dir,
# "containers" → install_containers_dir). Pre-Phase-5 manifests
# without a `:` default to scripts/.
local _eager _root _path _base
for _eager in "${LP_EAGER_FILES[@]}"; do
[[ -f "${install_scripts_dir}${_eager}" ]] || continue
if [[ "$_eager" == *:* ]]; then
_root="${_eager%%:*}"
_path="${_eager#*:}"
else
_root="scripts"; _path="$_eager"
fi
case "$_root" in
containers) _base="$install_containers_dir" ;;
*) _base="$install_scripts_dir" ;;
esac
[[ -f "${_base}${_path}" ]] || continue
if [[ "$LP_LOAD_TRACE" == "1" ]]; then
local _t0=$EPOCHREALTIME
source "${install_scripts_dir}${_eager}"
source "${_base}${_path}"
local _t1=$EPOCHREALTIME
local _ms
_ms=$(awk -v a="$_t0" -v b="$_t1" 'BEGIN{printf "%.3f", (b-a)*1000}')
printf '%s\t%s\n' "$_ms" "${_eager} (LAZY-EAGER)" >> "$LP_LOAD_TRACE_FILE"
printf '%s\t%s\n' "$_ms" "${_root}/${_path} (LAZY-EAGER)" >> "$LP_LOAD_TRACE_FILE"
else
source "${install_scripts_dir}${_eager}"
source "${_base}${_path}"
fi
done
else
@ -115,8 +127,21 @@ sourceInitilize()
done
fi
# Loading of all files
# Loading of all files. Three scans:
# libreportal_configs CFG_* vars from configs/* (~180 ms)
# app_configs CFG_<APP>_* vars from containers/* (~37 ms)
# containers Per-app installer functions (~70 ms)
#
# Under LP_LAZY=1 with a present manifest, the container scan is replaced
# by autoload stubs in function_manifest.sh — sourcing every container
# installer eagerly would defeat the lazy optimisation. The two config
# scans still run because bash can't lazy-load variables (Phase 6 will
# tackle this with a precompiled cache file).
sourceScanFiles "libreportal_configs";
sourceScanFiles "app_configs";
if [[ "$LP_LAZY" == "1" ]] && declare -p LP_FN_MAP >/dev/null 2>&1; then
: # containers covered by autoload stubs; skip the eager scan
else
sourceScanFiles "containers";
fi
}