GTA Online Startup Profiling Lab
Profile and optimize a Python simulation of GTA Online startup delays.
Introduction
30 Second Summary
A long loading screen gives you no clue about what is holding things up. Guessing at the cause can waste more time than the delay itself.
In this project, you will build a local Python investigation lab inspired by the 2021 GTA Online loading-time case study. Deterministic profiling exposes the two bottlenecks that guide your 10x startup redesign.
What You'll Build
When you run the finished lab, four startup paths print their timings so you can show exactly how repeated parsing plus quadratic duplicate checks stretch the online startup critical path.
By the end of this project, you'll have:
- A baseline loading comparison that makes story mode visibly faster than online mode. The online result still proves that all 6,000 records loaded.
- A profiler report that names scan_length plus dedupe_with_list among the costly functions. You can tie the loading gap to repeated parsing plus quadratic duplicate checks.
- An optimized four-way benchmark that shows one membership check per online item. A 10x startup plan that keeps noncritical catalog work away from the ready-to-play path.
- Secret Mission: A duplicate-injection test that proves dedupe_with_set preserves the original first-seen order while using fewer checks.
Are there any prerequisites?
You need a Mac with Python plus Visual Studio Code available. Comfort with Python plus algorithmic complexity keeps the focus on performance reasoning.
Before We Start
Your first checkpoint is to commit to a safe synthetic local performance investigation inspired by the GTA Online loading-time case study, without GTA files, game services, anti-cheat systems, cloud accounts, or external packages. Profiling identifies the real startup bottleneck before you redesign the path.
Verify the Mac Lab
Reliable performance measurements need a fixed runtime. This lab pins Python 3.14.7 so later timing changes reflect your code.
You'll verify the interpreter before creating the gta-loading-lab workspace in Visual Studio Code. A successful starter run gives every later benchmark a known starting point.
In this step, get ready to:
- Verify that Python 3.14.7 is available on your Mac.
- Create the gta-loading-lab workspace in Visual Studio Code.
- Run loading_lab.py from the workspace terminal.
Verify Python 3.14.7
The Python version is part of your test environment. Matching 3.14.7 makes later profiles reproducible on your Mac.
- Press Cmd+Space to open Spotlight Search.
- Type Terminal into Spotlight Search.
- Press Enter to open Terminal.
- Check the installed Python version by running:
python3 -VV
What does this command do?
- The python3 launcher starts the installed Python 3 interpreter.
- The -VV flag prints the detailed interpreter version.
✔️ I see Python 3.14.7
Your runtime is locked in. The output starts with Python 3.14.7 so you can continue below.
ⓧ I see an older version
Your current interpreter would make the later timing evidence inconsistent. Install Python 3.14.7 before collecting measurements.
- Download the official Python 3.14.7 macOS installer from the Python release page.
- Open python-3.14.7-macos11.pkg from your Downloads folder.
- Complete the installer prompts.
- Quit Terminal after the installation finishes.
- Press Cmd+Space to reopen Spotlight Search.
- Type Terminal into Spotlight Search.
- Press Enter to open a fresh Terminal window.
- Repeat the version check by running:
python3 -VV
What should I see?
The fresh Terminal session loads the updated interpreter path. Its output should start with Python 3.14.7.
ⓧ Command not found
Python is not available through the python3 launcher yet. The official macOS installer adds the required interpreter.
- Download the official Python 3.14.7 macOS installer from the Python release page.
- Open python-3.14.7-macos11.pkg from your Downloads folder.
- Complete the installer prompts.
- Quit Terminal after the installation finishes.
- Press Cmd+Space to reopen Spotlight Search.
- Type Terminal into Spotlight Search.
- Press Enter to open a fresh Terminal window.
- Verify the installation by running:
python3 -VV
What should I see?
A successful installation makes the launcher available. The output should start with Python 3.14.7.
Still seeing the wrong version?
An older Terminal session can retain its previous environment. Quit every Terminal window before opening a new one.
Confirm that the official installer completed before repeating the version check.
Help me verify Python 3.14.7 on my Mac.
Create the workspace
A workspace keeps the starter file beside the terminal that runs it. Placing it on your Desktop also makes the lab easy to find.
- Switch back to Terminal from the version check.
- Create the gta-loading-lab folder on your Desktop by running these commands:
cd ~/Desktop
mkdir gta-loading-lab
ls
cd gta-loading-lab
What do these commands do?
- The first command moves Terminal to your Desktop.
- The second command creates the gta-loading-lab folder.
- The ls command lists the Desktop contents so you can confirm the folder exists.
- The final command makes gta-loading-lab the active folder.
You should see gta-loading-lab in the ls output. Terminal now runs future commands from that folder.
Could not create the folder?
Terminal may report that the folder already exists. Use the existing folder only if it is empty and belongs to this project.
Help me create the gta-loading-lab folder.
- Press Cmd+Space to open Spotlight Search.
- Type Visual Studio Code into Spotlight Search.
- Press Enter to open Visual Studio Code.
- Click File in the menu bar.
- Click Open Folder....
- Select the gta-loading-lab folder on your Desktop.
- Click Open.
- Click Yes, I trust the authors if the Workspace Trust dialog appears.
Visual Studio Code should show gta-loading-lab at the top of the Explorer sidebar. This confirms the correct folder is open as your workspace.
Run the starter file
The starter script gives you a fast end-to-end check. A successful run proves that your editor and verified interpreter are using the same workspace.
- Select the Explorer view in the Activity Bar.
- Click New File....
- Enter loading_lab.py as the file name.
- Press Enter to create the file.
- Add the readiness check by pasting this code:
print("GTA loading lab ready")
What does this code do?
The print() call writes a readiness message to the terminal. That message proves Python successfully executed loading_lab.py.
- Save loading_lab.py.
✔️ Awesome, I've got everything!
Your starter file matches the lab checkpoint. Double check that loading_lab.py is saved.
ⓧ I'd like to double check the full code
print("GTA loading lab ready")
- Click View in the Visual Studio Code menu bar.
- Click Terminal to open the integrated terminal.
The integrated terminal starts inside the open gta-loading-lab workspace. Your script is ready to run from the folder that contains it.
Before you run it, which line should confirm that the editor and interpreter are working together?
- Run the saved starter from the workspace terminal by entering:
python3 loading_lab.py
What does this command do?
The command asks your verified Python interpreter to execute loading_lab.py from the current workspace. The script writes its readiness message to the terminal.
You'll see GTA loading lab ready on its own line. That is your first working checkpoint.
The starter now runs inside the verified lab.
Don't see the readiness message?
Check that the Explorer sidebar shows loading_lab.py directly inside gta-loading-lab.
Confirm that the file contains the same single line shown in the full-code tab. Save it before running the command again.
Help me run the loading lab starter.
Your runtime and workspace are ready. Next up, you'll turn this one-line starter into a simulation that exposes the story-versus-online loading gap.
Reproduce the Loading Gap
Your Python starter runs successfully. It now needs a visible performance problem that you can investigate with evidence.
This step puts extra catalog processing on the online startup path. The story path stays lightweight. The resulting gap gives you a concrete failure to profile.
In this step, get ready to:
- Generate separate story and online catalog payloads.
- Model repeated parsing work and list-backed duplicate detection.
- Run the baseline comparison and observe the loading gap.
Generate the synthetic catalogs
The simulator uses synthetic JSON records so you can study the startup pattern locally. Story mode receives a small shared payload. Online mode receives the full catalog.
- Switch back to loading_lab.py in Visual Studio Code.
- Place your cursor above the existing print("GTA loading lab ready") line.
- Add the imports, baseline settings, and payload builder by copying this code:
import json
import sys
import time
DEFAULT_ITEMS = 6000
PARSER_RESCANS = 12
VALID_MODES = {
"help",
"baseline",
}
def build_payload(item_count, prefix):
items = [
{
"key": f"{prefix}_{index:06d}",
"price": 1000 + (index % 50000),
"category": prefix.lower(),
}
for index in range(item_count)
]
return json.dumps(items, separators=(",", ":"))
What does this code do?
- The constants make 6,000 records the default catalog size. They also set the repeated scan count to 12.
- build_payload() creates records with unique keys, prices, and categories.
- json.dumps() turns those records into a compact serialized payload for the startup simulation.
- Save loading_lab.py.
- Confirm the expanded file still runs by using this command in the workspace terminal:
python3 loading_lab.py
You should still see GTA loading lab ready. This confirms the new payload code loads without a syntax error.
Does the starter message disappear?
Check that the original print("GTA loading lab ready") line remains at the bottom of the file. Make sure each dictionary entry inside build_payload() ends with a comma.
Compare the indentation of the list comprehension with the code block above.
Help me fix the payload builder in loading_lab.py.
The naive parser needs to imitate repeated length scans over the same serialized payload. scan_length() walks through every character once. parse_catalog_naive() repeats that work before decoding the JSON.
- Add the repeated-scan helpers below build_payload() by copying this code:
def scan_length(text):
count = 0
for _character in text:
count += 1
return count
def parse_catalog_naive(payload):
observed_length = 0
for _scan in range(PARSER_RESCANS):
observed_length = scan_length(payload)
return json.loads(payload), observed_length
How does the repeated scan work?
- scan_length() increments a counter for every character in the payload.
- parse_catalog_naive() invokes that scan 12 times through PARSER_RESCANS.
- json.loads() finally turns the serialized payload back into catalog records.
- Save loading_lab.py.
- Check the new helpers for syntax errors by running the starter again:
python3 loading_lab.py
You should see GTA loading lab ready again. The parser helpers are now loaded successfully.
Seeing a parser syntax error?
Check that return json.loads(payload), observed_length lines up with the for statement. It belongs outside the loop.
Confirm that PARSER_RESCANS matches the constant near the top of the file.
Help me fix the repeated parser scan.
Model the quadratic online path
The online path checks every incoming key against the records already accepted. The accepted list grows after each unique item. That nested search produces O(n²) comparison growth.
- Add the list-backed duplicate detector below parse_catalog_naive() by copying this code:
def dedupe_with_list(items):
unique_items = []
comparisons = 0
for item in items:
for existing in unique_items:
comparisons += 1
if existing["key"] == item["key"]:
break
else:
unique_items.append(item)
return unique_items, comparisons
What does the duplicate detector measure?
- unique_items preserves the records in their first-seen order.
- The inner loop compares each incoming key with every accepted key until it finds a match.
- comparisons records the amount of duplicate-checking work performed.
- Save loading_lab.py.
- Confirm the duplicate detector loads by running the starter:
python3 loading_lab.py
You should see GTA loading lab ready. The list-backed detector is now part of the script.
Does the duplicate detector fail to load?
Check that the else: line aligns with the inner for existing in unique_items: loop. This loop-level branch adds the item only when no matching key causes a break.
Help me fix dedupe_with_list().
Each startup path needs its own timing boundary. The story path only decodes its small payload. Its result becomes the reference point for the heavier online path.
- Add the story-mode runner below dedupe_with_list() by copying this code:
def run_story(payload):
started = time.perf_counter()
items = json.loads(payload)
finished = time.perf_counter()
return {
"label": "story",
"total_seconds": finished - started,
"parse_seconds": finished - started,
"index_seconds": 0.0,
"item_count": len(items),
"checks": 0,
}
How is story startup measured?
- time.perf_counter() captures timestamps around the JSON decoding work.
- The result stores separate total, parse, and indexing metrics.
- The story path reports zero indexing checks because it does not process the extra online catalog.
- Save loading_lab.py.
- Confirm the story runner loads by running the starter:
python3 loading_lab.py
You should see the existing readiness message with no traceback. The story timing function is ready for the baseline command.
Seeing an error in run_story()?
Confirm every result key is inside the returned dictionary. Check that the closing brace aligns with return {.
Help me repair run_story().
The online runner measures parsing and duplicate detection separately. This separation exposes which part of the startup path consumes the elapsed time.
- Add the online-mode runner below run_story() by copying this code:
def run_online(label, payload, parser, deduper):
total_started = time.perf_counter()
parse_started = time.perf_counter()
items, _payload_length = parser(payload)
parse_finished = time.perf_counter()
index_started = time.perf_counter()
unique_items, checks = deduper(items)
index_finished = time.perf_counter()
return {
"label": label,
"total_seconds": index_finished - total_started,
"parse_seconds": parse_finished - parse_started,
"index_seconds": index_finished - index_started,
"item_count": len(unique_items),
"checks": checks,
}
How are the online costs separated?
- parser receives the serialized payload and returns decoded records.
- deduper processes those records and returns its operation count.
- The returned dictionary keeps parsing time separate from indexing time.
- Save loading_lab.py.
- Confirm the online runner loads by running the starter:
python3 loading_lab.py
You should see GTA loading lab ready with no traceback. Both timed startup paths now load successfully.
Does run_online() raise an error?
Check that each timer variable uses the same spelling in the returned dictionary. Confirm that parser(payload) returns two values.
Help me fix the online timing path.
The final reporting helpers create both payload sizes and format their measurements consistently. A consistent output makes the loading gap easy to compare.
- Add the payload coordinator and result printer below run_online() by copying this code:
def make_payloads(item_count):
story_item_count = max(50, item_count // 30)
story_payload = build_payload(story_item_count, "STORY")
online_payload = build_payload(item_count, "ONLINE")
return story_payload, online_payload
def print_results(results):
print("\nStartup results")
print("=" * 92)
for result in results:
print(
f"{result['label']:<18} "
f"total={result['total_seconds']:.4f}s "
f"parse={result['parse_seconds']:.4f}s "
f"index={result['index_seconds']:.4f}s "
f"items={result['item_count']:,} "
f"checks={result['checks']:,}"
)
print("=" * 92)
How are the results prepared?
- make_payloads() limits story mode to a small fraction of the requested item count.
- The online payload receives the complete requested item count.
- print_results() prints total time, parse time, index time, item count, and check count for each path.
- Save loading_lab.py.
- Check the reporting helpers by running the starter:
python3 loading_lab.py
You should see the readiness message again. The script can now build both payloads and format the future comparison.
Seeing a formatting error?
Check that every formatted result key uses matching single quotes inside the braces. Confirm that each formatted string remains inside the print() call.
Help me repair print_results().
Run the baseline comparison
The baseline command now needs to connect the payloads to their startup paths. It runs story mode once. It runs the intentionally expensive online path with repeated scans and list-backed duplicate detection.
- Add the baseline coordinator below print_results() by copying this code:
def run_baseline(item_count):
story_payload, online_payload = make_payloads(item_count)
results = [
run_story(story_payload),
run_online(
"online-naive",
online_payload,
parse_catalog_naive,
dedupe_with_list,
),
]
print_results(results)
What does the baseline compare?
- run_story() measures the small story payload.
- run_online() receives parse_catalog_naive and dedupe_with_list as the intentionally slow online implementation.
- print_results() places both measurements in one table.
- Save loading_lab.py.
- Confirm the baseline coordinator loads by running the starter:
python3 loading_lab.py
You should still see GTA loading lab ready. Every function required by the baseline comparison now loads.
Does run_baseline() fail to load?
Check that parse_catalog_naive and dedupe_with_list are passed without parentheses. The online runner calls them with the required data.
Help me connect the baseline functions.
The command-line layer reads the requested mode and catalog size from sys.argv. It gives the script a repeatable way to run the same workload.
- Add the baseline help text and argument parser below run_baseline() by copying this code:
def print_help():
print("Usage:")
print(" python3 loading_lab.py baseline --items 6000")
def parse_cli(argv):
if len(argv) < 2:
return "help", DEFAULT_ITEMS, 10
mode = argv[1]
if mode not in VALID_MODES:
raise SystemExit(f"Unknown mode: {mode}")
item_count = DEFAULT_ITEMS
growth = 10
index = 2
while index < len(argv):
option = argv[index]
value = argv[index + 1]
if option == "--items":
item_count = int(value)
else:
raise SystemExit(f"Unknown option: {option}")
index += 2
return mode, item_count, growth
How does the command-line layer work?
- parse_cli() reads the mode from the first command-line argument.
- The --items option replaces the default catalog size.
- DEFAULT_ITEMS keeps the workload at 6,000 records when no item count is supplied.
- Save loading_lab.py.
- Confirm the command-line helpers load by running the starter:
python3 loading_lab.py
You should see GTA loading lab ready one final time. The next edit replaces that temporary checkpoint with the real program entry point.
Does the argument parser fail?
Check that index += 2 remains inside the while loop. Confirm that the function returns mode, item_count, and growth.
Help me fix parse_cli().
- Find the original print("GTA loading lab ready") line at the bottom of loading_lab.py.
- Replace that line with the program entry point below:
def main():
mode, item_count, growth = parse_cli(sys.argv)
if mode == "help":
print_help()
elif mode == "baseline":
run_baseline(item_count)
if __name__ == "__main__":
main()
How does the program start?
- main() receives the selected mode and item count from parse_cli().
- The baseline branch calls run_baseline() with the requested workload.
- The final condition runs main() when you execute loading_lab.py as a script.
- Save loading_lab.py.
Before you run the finished lab, which path do you expect to take longer when the online catalog contains 6,000 items?
- Run the completed baseline comparison in the workspace terminal with this command:
python3 loading_lab.py baseline --items 6000
You should see one story row and one online-naive row. Both rows report valid item counts.
The online row should take longer. It should report items=6,000 and checks=17,997,000.
Why are there 17,997,000 comparisons?
Every unique online item scans the list of previously accepted items. The comparison total follows the triangular sum from zero through 5,999.
That operation count is deterministic even when the elapsed times vary between runs. It makes the quadratic behavior visible before profiling begins.
Missing the two startup rows?
Check that you replaced the original readiness print with the complete main() block. Confirm that the command includes baseline and the --items option.
If the check count differs, compare the nesting inside dedupe_with_list() with the full code below.
Help me reproduce the baseline loading gap.
✔️ Awesome, I've got everything!
Great work. Your lab now reproduces a measurable story-versus-online startup gap with deterministic duplicate-comparison evidence.
ⓧ I'd like to double check the full code
import json
import sys
import time
DEFAULT_ITEMS = 6000
PARSER_RESCANS = 12
VALID_MODES = {
"help",
"baseline",
}
def build_payload(item_count, prefix):
items = [
{
"key": f"{prefix}_{index:06d}",
"price": 1000 + (index % 50000),
"category": prefix.lower(),
}
for index in range(item_count)
]
return json.dumps(items, separators=(",", ":"))
def scan_length(text):
count = 0
for _character in text:
count += 1
return count
def parse_catalog_naive(payload):
observed_length = 0
for _scan in range(PARSER_RESCANS):
observed_length = scan_length(payload)
return json.loads(payload), observed_length
def dedupe_with_list(items):
unique_items = []
comparisons = 0
for item in items:
for existing in unique_items:
comparisons += 1
if existing["key"] == item["key"]:
break
else:
unique_items.append(item)
return unique_items, comparisons
def run_story(payload):
started = time.perf_counter()
items = json.loads(payload)
finished = time.perf_counter()
return {
"label": "story",
"total_seconds": finished - started,
"parse_seconds": finished - started,
"index_seconds": 0.0,
"item_count": len(items),
"checks": 0,
}
def run_online(label, payload, parser, deduper):
total_started = time.perf_counter()
parse_started = time.perf_counter()
items, _payload_length = parser(payload)
parse_finished = time.perf_counter()
index_started = time.perf_counter()
unique_items, checks = deduper(items)
index_finished = time.perf_counter()
return {
"label": label,
"total_seconds": index_finished - total_started,
"parse_seconds": parse_finished - parse_started,
"index_seconds": index_finished - index_started,
"item_count": len(unique_items),
"checks": checks,
}
def make_payloads(item_count):
story_item_count = max(50, item_count // 30)
story_payload = build_payload(story_item_count, "STORY")
online_payload = build_payload(item_count, "ONLINE")
return story_payload, online_payload
def print_results(results):
print("\nStartup results")
print("=" * 92)
for result in results:
print(
f"{result['label']:<18} "
f"total={result['total_seconds']:.4f}s "
f"parse={result['parse_seconds']:.4f}s "
f"index={result['index_seconds']:.4f}s "
f"items={result['item_count']:,} "
f"checks={result['checks']:,}"
)
print("=" * 92)
def run_baseline(item_count):
story_payload, online_payload = make_payloads(item_count)
results = [
run_story(story_payload),
run_online(
"online-naive",
online_payload,
parse_catalog_naive,
dedupe_with_list,
),
]
print_results(results)
def print_help():
print("Usage:")
print(" python3 loading_lab.py baseline --items 6000")
def parse_cli(argv):
if len(argv) < 2:
return "help", DEFAULT_ITEMS, 10
mode = argv[1]
if mode not in VALID_MODES:
raise SystemExit(f"Unknown mode: {mode}")
item_count = DEFAULT_ITEMS
growth = 10
index = 2
while index < len(argv):
option = argv[index]
value = argv[index + 1]
if option == "--items":
item_count = int(value)
else:
raise SystemExit(f"Unknown option: {option}")
index += 2
return mode, item_count, growth
def main():
mode, item_count, growth = parse_cli(sys.argv)
if mode == "help":
print_help()
elif mode == "baseline":
run_baseline(item_count)
if __name__ == "__main__":
main()
You have reproduced the performance failure with timings and operation counts. Next, you will profile the slow path to identify which functions own that cost.
Profile the Startup Path
Your baseline makes the online startup delay visible. A timer alone cannot show which function consumes that time.
CPU, disk, or network delays can produce the same slow loading screen. In this step, deterministic profiling traces the local startup critical path so you can classify the real bottlenecks before changing the design.
In this step, get ready to:
- Capture a cumulative-time profile of the naive online startup path.
- Interpret the profiler's call counts and cumulative-time values.
- Classify the cause of each hot path.
Profile the naive online path
Python's cProfile records function calls during one complete run. Sorting by cumulative time brings expensive call chains near the top of the report.
Before you run it, which function do you expect to own more cumulative time.
- Keep loading_lab.py unchanged so the profiler measures the same baseline implementation.
- Profile the naive online path from the Terminal panel by running this command:
python3 -m cProfile -s cumulative loading_lab.py online-naive --items 6000
What Does This Command Measure?
- The -m cProfile option runs loading_lab.py through Python's deterministic profiler.
- The -s cumulative option sorts functions by time spent in each function plus its subfunctions.
- The online-naive mode isolates the intentionally slow online startup path.
- The profiler locates expensive functions. Your unprofiled baseline remains the elapsed-time comparison.
- Wait for the profiled startup run to finish.
- Confirm the report contains ncalls, tottime, and cumtime columns.
The usual startup metrics appear before the profiler table. You now have function-level evidence from the same slow path you measured earlier.
Profiler Report Missing?
- Return to the integrated Terminal for gta-loading-lab if Terminal cannot locate loading_lab.py.
- Confirm the command uses the online-naive mode.
- Confirm the command includes the --items option with the value 6000.
Help me diagnose my profiler output.
Interpret the hot paths
The call counts expose repeated work. The source code reveals the algorithmic shape behind that work.
Before you compare the rows, predict whether 12 payload scans or 17,997,000 duplicate comparisons will dominate cumulative time.
- Locate the profiler row ending in scan_length.
- Read its ncalls value.
- Locate the profiler row ending in dedupe_with_list.
- Read its cumtime value.
- Compare both rows with the baseline's 17,997,000 duplicate comparisons.
You should see scan_length called 12 times. Its cumulative time captures repeated scans of the same payload.
The dedupe_with_list row appears as one function call. Its cumulative time includes the millions of comparisons performed inside its nested loops.
What Does the Profile Prove?
- The scan_length function walks the same serialized JSON payload once per rescan. This is repeated CPU parsing work.
- The dedupe_with_list function searches a growing list for every input item. This is an O(n²) data-structure bottleneck.
- The simulator builds its catalog locally. This startup path makes no network request.
You should see dedupe_with_list and scan_length among the highest cumulative-time functions. That evidence connects the startup delay to local CPU work.
The profiler has replaced guesswork with evidence. Next, you'll remove each hot path separately so every speedup has a measurable cause.
Remove the Hot Paths
Your cProfile report turned a slow startup into two measurable CPU costs. It tied the delay to list-based duplicate detection plus repeated payload scanning.
The two costs are independent. A four-way benchmark isolates the speedup from indexing. It also isolates the speedup from caching.
In this step, get ready to:
- Preserve first-seen records with a set-backed index.
- Calculate the payload length once per parse.
- Benchmark the naive path against both optimizations.
Replace the linear membership scan
A set stores each accepted key for membership checks. A separate list keeps the catalog records in first-seen order.
- In loading_lab.py, locate the dedupe_with_list function.
- Add dedupe_with_set directly below it by pasting this code:
def dedupe_with_set(items):
unique_items = []
seen_keys = set()
membership_checks = 0
for item in items:
membership_checks += 1
key = item["key"]
if key in seen_keys:
continue
seen_keys.add(key)
unique_items.append(item)
return unique_items, membership_checks
How does the index preserve behavior?
- The seen_keys set tracks keys that have already appeared.
- The unique_items list preserves the first accepted record for each key.
- The membership_checks counter records one lookup for every input item.
- Save loading_lab.py.
- Confirm Visual Studio Code shows no syntax diagnostics under dedupe_with_set.
Seeing a syntax diagnostic?
Check that dedupe_with_set sits after the complete dedupe_with_list function. Make sure the two functions have no shared indentation.
Compare the spelling of unique_items, seen_keys, and membership_checks.
Help me fix my set-backed duplicate function.
Cache the repeated payload length
The naive parser calls scan_length for every configured rescan. Caching lets the optimized parser calculate len(payload) once.
- In loading_lab.py, locate the parse_catalog_naive function.
- Add parse_catalog_cached directly below it by pasting this code:
def parse_catalog_cached(payload):
observed_length = len(payload)
for _scan in range(PARSER_RESCANS):
_cached_length = observed_length
return json.loads(payload), observed_length
What does the cached parser change?
- The first line calculates the payload length once.
- Each simulated rescan reuses observed_length instead of traversing the payload.
- The function still returns decoded records plus the observed payload length.
- Save loading_lab.py.
- Check the existing naive path for regressions by running this command:
python3 loading_lab.py online-naive --items 6000
What should you see?
You should see an online-naive result with 6,000 items. Its check count still reaches millions because this command deliberately uses the original list-backed path.
This successful run confirms both new functions were added without breaking the existing simulator.
Did the naive path stop running?
Check that parse_catalog_cached begins at the left edge of the file. An extra indent can place it inside parse_catalog_naive.
Make sure json.loads(payload) remains inside the return statement.
Help me restore the runnable naive path.
Benchmark all four startup paths
A controlled comparison changes one hot path at a time. The indexed mode replaces duplicate scanning first. The optimized mode also removes repeated payload scans.
- In loading_lab.py, locate the run_online_naive function.
- Add run_comparison directly below it by pasting this code:
def run_comparison(item_count):
story_payload, online_payload = make_payloads(item_count)
results = [
run_story(story_payload),
run_online(
"online-naive",
online_payload,
parse_catalog_naive,
dedupe_with_list,
),
run_online(
"online-indexed",
online_payload,
parse_catalog_naive,
dedupe_with_set,
),
run_online(
"online-optimized",
online_payload,
parse_catalog_cached,
dedupe_with_set,
),
]
print_results(results)
How does the comparison isolate each change?
- The story path remains the lightweight reference.
- The online-naive path keeps both profiled hot paths.
- The online-indexed path changes only the duplicate data structure.
- The online-optimized path combines set-backed indexing with the cached parser.
- Save loading_lab.py.
- Confirm Visual Studio Code shows no syntax diagnostics inside run_comparison.
- Update the command-routing sections in loading_lab.py to match this code:
VALID_MODES = {
"help",
"baseline",
"online-naive",
"compare",
}
def print_help():
print("Usage:")
print(" python3 loading_lab.py baseline --items 6000")
print(" python3 loading_lab.py online-naive --items 6000")
print(" python3 loading_lab.py compare --items 6000")
def main():
mode, item_count, growth = parse_cli(sys.argv)
if mode == "help":
print_help()
elif mode == "baseline":
run_baseline(item_count)
elif mode == "online-naive":
run_online_naive(item_count)
elif mode == "compare":
run_comparison(item_count)
How does the compare command reach the benchmark?
- The VALID_MODES set accepts compare as a command mode.
- The help output shows the command learners can run.
- The final branch passes item_count into run_comparison.
- Save loading_lab.py.
Before you run the comparison, which online path do you expect to be fastest? Hold that answer in mind.
- Run the four-way benchmark with 6,000 online items by entering this command:
python3 loading_lab.py compare --items 6000
What should the benchmark prove?
- The online-naive row should report 6,000 items plus millions of duplicate comparisons.
- The online-indexed row should report 6,000 items plus 6,000 membership checks. Its improvement isolates the set-backed index.
- The online-optimized row should also report 6,000 membership checks. Its lower parse time isolates the cached payload length.
- The online-optimized row should have the fastest total time among the online paths.
That closes the investigation loop. Each optimization now has measured evidence tied to the hot path it removes.
Compare command not working?
If the command reports an unknown mode, check that "compare" appears inside VALID_MODES.
If Python cannot find run_comparison, make sure the function is defined above main.
Help me debug the four-way comparison.
✔️ Awesome, I've got everything!
Great. Save loading_lab.py and keep the four-way benchmark output visible for your checkpoint.
ⓧ I'd like to double check the full code
import json
import sys
import time
DEFAULT_ITEMS = 6000
PARSER_RESCANS = 12
VALID_MODES = {
"help",
"baseline",
"online-naive",
"compare",
}
def build_payload(item_count, prefix):
items = [
{
"key": f"{prefix}_{index:06d}",
"price": 1000 + (index % 50000),
"category": prefix.lower(),
}
for index in range(item_count)
]
return json.dumps(items, separators=(",", ":"))
def scan_length(text):
count = 0
for _character in text:
count += 1
return count
def parse_catalog_naive(payload):
observed_length = 0
for _scan in range(PARSER_RESCANS):
observed_length = scan_length(payload)
return json.loads(payload), observed_length
def parse_catalog_cached(payload):
observed_length = len(payload)
for _scan in range(PARSER_RESCANS):
_cached_length = observed_length
return json.loads(payload), observed_length
def dedupe_with_list(items):
unique_items = []
comparisons = 0
for item in items:
for existing in unique_items:
comparisons += 1
if existing["key"] == item["key"]:
break
else:
unique_items.append(item)
return unique_items, comparisons
def dedupe_with_set(items):
unique_items = []
seen_keys = set()
membership_checks = 0
for item in items:
membership_checks += 1
key = item["key"]
if key in seen_keys:
continue
seen_keys.add(key)
unique_items.append(item)
return unique_items, membership_checks
def run_story(payload):
started = time.perf_counter()
items = json.loads(payload)
finished = time.perf_counter()
return {
"label": "story",
"total_seconds": finished - started,
"parse_seconds": finished - started,
"index_seconds": 0.0,
"item_count": len(items),
"checks": 0,
}
def run_online(label, payload, parser, deduper):
total_started = time.perf_counter()
parse_started = time.perf_counter()
items, _payload_length = parser(payload)
parse_finished = time.perf_counter()
index_started = time.perf_counter()
unique_items, checks = deduper(items)
index_finished = time.perf_counter()
return {
"label": label,
"total_seconds": index_finished - total_started,
"parse_seconds": parse_finished - parse_started,
"index_seconds": index_finished - index_started,
"item_count": len(unique_items),
"checks": checks,
}
def make_payloads(item_count):
story_item_count = max(50, item_count // 30)
story_payload = build_payload(story_item_count, "STORY")
online_payload = build_payload(item_count, "ONLINE")
return story_payload, online_payload
def print_results(results):
print("\nStartup results")
print("=" * 92)
for result in results:
print(
f"{result['label']:<18} "
f"total={result['total_seconds']:.4f}s "
f"parse={result['parse_seconds']:.4f}s "
f"index={result['index_seconds']:.4f}s "
f"items={result['item_count']:,} "
f"checks={result['checks']:,}"
)
print("=" * 92)
def run_baseline(item_count):
story_payload, online_payload = make_payloads(item_count)
results = [
run_story(story_payload),
run_online(
"online-naive",
online_payload,
parse_catalog_naive,
dedupe_with_list,
),
]
print_results(results)
def run_online_naive(item_count):
online_payload = build_payload(item_count, "ONLINE")
result = run_online(
"online-naive",
online_payload,
parse_catalog_naive,
dedupe_with_list,
)
print_results([result])
def run_comparison(item_count):
story_payload, online_payload = make_payloads(item_count)
results = [
run_story(story_payload),
run_online(
"online-naive",
online_payload,
parse_catalog_naive,
dedupe_with_list,
),
run_online(
"online-indexed",
online_payload,
parse_catalog_naive,
dedupe_with_set,
),
run_online(
"online-optimized",
online_payload,
parse_catalog_cached,
dedupe_with_set,
),
]
print_results(results)
def print_help():
print("Usage:")
print(" python3 loading_lab.py baseline --items 6000")
print(" python3 loading_lab.py online-naive --items 6000")
print(" python3 loading_lab.py compare --items 6000")
def parse_cli(argv):
if len(argv) < 2:
return "help", DEFAULT_ITEMS, 10
mode = argv[1]
if mode not in VALID_MODES:
raise SystemExit(f"Unknown mode: {mode}")
item_count = DEFAULT_ITEMS
growth = 10
index = 2
while index < len(argv):
option = argv[index]
if index + 1 >= len(argv):
raise SystemExit(f"Missing value for {option}")
value = argv[index + 1]
if option == "--items":
item_count = int(value)
elif option == "--growth":
growth = int(value)
else:
raise SystemExit(f"Unknown option: {option}")
index += 2
if item_count <= 0 or growth <= 0:
raise SystemExit("--items and --growth must be positive integers")
return mode, item_count, growth
def main():
mode, item_count, growth = parse_cli(sys.argv)
if mode == "help":
print_help()
elif mode == "baseline":
run_baseline(item_count)
elif mode == "online-naive":
run_online_naive(item_count)
elif mode == "compare":
run_comparison(item_count)
if __name__ == "__main__":
main()
Your simulator now turns profiler evidence into two isolated speedups. Next, you will test whether the design can survive a catalog that grows by 10x.
Redesign for a 10x Catalog
Your four-way benchmark proved that cached parsing plus a set-backed index makes the current catalog faster. A catalog that grows by 10x can still overwhelm the startup critical path when every record remains synchronous.
This step projects that growth without running the dangerous quadratic path. You will move noncritical work beyond the playable milestone and enforce a local performance budget.
In this step, get ready to:
- Calculate the effect of a 10x catalog without executing the projected quadratic workload.
- Document a startup architecture that keeps optional catalog work outside the playable path.
- Evaluate the optimized startup against a one-second local budget.
Project the 10x growth
A mathematical projection exposes the scaling risk without making your Mac perform billions of comparisons. The command will calculate the expected work before timing only the optimized 6,000-item path.
- In loading_lab.py, find the configuration block near the top of the file:
DEFAULT_ITEMS = 6000
PARSER_RESCANS = 12
VALID_MODES = {
"help",
"baseline",
"online-naive",
"compare",
}
What Are You Changing?
This block defines the default catalog size and the commands accepted by the lab. The projection needs its own startup budget plus a new command mode.
- Replace that configuration block with this version:
DEFAULT_ITEMS = 6000
PARSER_RESCANS = 12
STARTUP_BUDGET_SECONDS = 1.0
VALID_MODES = {
"help",
"baseline",
"online-naive",
"compare",
"project",
}
How Does the Budget Work?
The lab treats 1.0 second as a local regression threshold. The project mode uses that threshold to classify the current optimized result.
- Add these growth calculations above print_help() in loading_lab.py by copying this first part:
def expected_list_comparisons(item_count):
return item_count * (item_count - 1) // 2
def project_growth(item_count, growth):
projected_items = item_count * growth
current_comparisons = expected_list_comparisons(item_count)
projected_comparisons = expected_list_comparisons(projected_items)
comparison_growth = (
projected_comparisons / current_comparisons
if current_comparisons
else 0.0
)
online_payload = build_payload(item_count, "ONLINE")
optimized = run_online(
"online-optimized",
online_payload,
parse_catalog_cached,
dedupe_with_set,
)
budget_status = (
"PASS"
if optimized["total_seconds"] <= STARTUP_BUDGET_SECONDS
else "REVIEW"
)
What Do These Calculations Do?
- The comparison formula calculates the exact work produced by checking every unique item against all previously accepted items.
- The projected count scales the catalog size without building or processing the larger payload.
- The optimized run measures the current implementation against the local startup budget.
- Continue project_growth() directly below the budget_status block with this output section:
print("\n10x growth projection")
print("=" * 72)
print(f"Current catalog items: {item_count:,}")
print(f"Projected catalog items: {projected_items:,}")
print(f"Current naive comparisons: {current_comparisons:,}")
print(f"Projected naive comparisons: {projected_comparisons:,}")
print(f"Growth in naive comparison work: {comparison_growth:.1f}x")
print(
f"Current optimized startup: {optimized['total_seconds']:.4f}s "
f"against {STARTUP_BUDGET_SECONDS:.1f}s budget: {budget_status}"
)
print("\nRedesign controls")
print("1. Precompute a normalized, versioned catalog artifact before launch.")
print("2. Load only a minimal playable partition on the critical path.")
print("3. Cache the parsed snapshot and rebuild a set-backed key index by version.")
print("4. Initialize noncritical catalog partitions in the background after ready-to-play.")
print("5. Track ready-to-play and fully-ready budgets separately in regression tests.")
What Will the Report Show?
The report places the current and projected comparison counts side by side. It also prints the optimized timing and five controls that keep future startup work bounded.
- Replace the existing print_help() function with this version:
def print_help():
print("Usage:")
print(" python3 loading_lab.py baseline --items 6000")
print(" python3 loading_lab.py online-naive --items 6000")
print(" python3 loading_lab.py compare --items 6000")
print(" python3 loading_lab.py project --items 6000 --growth 10")
Why Update the Help Output?
The help output now exposes the new projection command. It gives the learner-facing interface the same modes that the program accepts.
- Replace the existing parse_cli() function with this version:
def parse_cli(argv):
if len(argv) < 2:
return "help", DEFAULT_ITEMS, 10
mode = argv[1]
if mode not in VALID_MODES:
raise SystemExit(f"Unknown mode: {mode}")
item_count = DEFAULT_ITEMS
growth = 10
index = 2
while index < len(argv):
option = argv[index]
if index + 1 >= len(argv):
raise SystemExit(f"Missing value for {option}")
value = argv[index + 1]
if option == "--items":
item_count = int(value)
elif option == "--growth":
growth = int(value)
else:
raise SystemExit(f"Unknown option: {option}")
index += 2
if item_count <= 0 or growth <= 0:
raise SystemExit("--items and --growth must be positive integers")
return mode, item_count, growth
How Is Growth Parsed?
- The parser starts with a default growth factor of 10.
- The --growth option lets the command replace that default.
- The final check prevents zero or negative catalog sizes from reaching the projection.
- Replace the existing main() function with this version:
def main():
mode, item_count, growth = parse_cli(sys.argv)
if mode == "help":
print_help()
elif mode == "baseline":
run_baseline(item_count)
elif mode == "online-naive":
run_online_naive(item_count)
elif mode == "compare":
run_comparison(item_count)
elif mode == "project":
project_growth(item_count, growth)
How Does the Command Reach the Projection?
The command dispatcher now receives the growth factor from parse_cli(). Selecting project sends both values to project_growth().
- Save loading_lab.py.
- Run the 10x projection from the workspace Terminal with this command:
python3 loading_lab.py project --items 6000 --growth 10
What Does This Command Test?
The command projects the naive comparison count for 60,000 items. It times the optimized path with 6,000 items so the quadratic projection never runs.
You should see 60,000 projected items and about 100.0x growth in naive comparison work. The optimized result ends with either PASS or REVIEW for your Mac.
Projection Command Not Running?
Check that project appears in VALID_MODES. Confirm that main() calls project_growth(item_count, growth).
Make sure both new functions sit above print_help() with consistent indentation.
Help me debug the 10x projection command.
✔️ Awesome, I've got everything!
Great. Save loading_lab.py once more before moving to the architecture document.
ⓧ I'd like to double check the full code
import json
import sys
import time
DEFAULT_ITEMS = 6000
PARSER_RESCANS = 12
STARTUP_BUDGET_SECONDS = 1.0
VALID_MODES = {
"help",
"baseline",
"online-naive",
"compare",
"project",
}
def build_payload(item_count, prefix):
items = [
{
"key": f"{prefix}_{index:06d}",
"price": 1000 + (index % 50000),
"category": prefix.lower(),
}
for index in range(item_count)
]
return json.dumps(items, separators=(",", ":"))
def scan_length(text):
count = 0
for _character in text:
count += 1
return count
def parse_catalog_naive(payload):
observed_length = 0
for _scan in range(PARSER_RESCANS):
observed_length = scan_length(payload)
return json.loads(payload), observed_length
def parse_catalog_cached(payload):
observed_length = len(payload)
for _scan in range(PARSER_RESCANS):
_cached_length = observed_length
return json.loads(payload), observed_length
def dedupe_with_list(items):
unique_items = []
comparisons = 0
for item in items:
for existing in unique_items:
comparisons += 1
if existing["key"] == item["key"]:
break
else:
unique_items.append(item)
return unique_items, comparisons
def dedupe_with_set(items):
unique_items = []
seen_keys = set()
membership_checks = 0
for item in items:
membership_checks += 1
key = item["key"]
if key in seen_keys:
continue
seen_keys.add(key)
unique_items.append(item)
return unique_items, membership_checks
def run_story(payload):
started = time.perf_counter()
items = json.loads(payload)
finished = time.perf_counter()
return {
"label": "story",
"total_seconds": finished - started,
"parse_seconds": finished - started,
"index_seconds": 0.0,
"item_count": len(items),
"checks": 0,
}
def run_online(label, payload, parser, deduper):
total_started = time.perf_counter()
parse_started = time.perf_counter()
items, _payload_length = parser(payload)
parse_finished = time.perf_counter()
index_started = time.perf_counter()
unique_items, checks = deduper(items)
index_finished = time.perf_counter()
return {
"label": label,
"total_seconds": index_finished - total_started,
"parse_seconds": parse_finished - parse_started,
"index_seconds": index_finished - index_started,
"item_count": len(unique_items),
"checks": checks,
}
def make_payloads(item_count):
story_item_count = max(50, item_count // 30)
story_payload = build_payload(story_item_count, "STORY")
online_payload = build_payload(item_count, "ONLINE")
return story_payload, online_payload
def print_results(results):
print("\nStartup results")
print("=" * 92)
for result in results:
print(
f"{result['label']:<18} "
f"total={result['total_seconds']:.4f}s "
f"parse={result['parse_seconds']:.4f}s "
f"index={result['index_seconds']:.4f}s "
f"items={result['item_count']:,} "
f"checks={result['checks']:,}"
)
print("=" * 92)
def run_baseline(item_count):
story_payload, online_payload = make_payloads(item_count)
results = [
run_story(story_payload),
run_online(
"online-naive",
online_payload,
parse_catalog_naive,
dedupe_with_list,
),
]
print_results(results)
def run_online_naive(item_count):
online_payload = build_payload(item_count, "ONLINE")
result = run_online(
"online-naive",
online_payload,
parse_catalog_naive,
dedupe_with_list,
)
print_results([result])
def run_comparison(item_count):
story_payload, online_payload = make_payloads(item_count)
results = [
run_story(story_payload),
run_online(
"online-naive",
online_payload,
parse_catalog_naive,
dedupe_with_list,
),
run_online(
"online-indexed",
online_payload,
parse_catalog_naive,
dedupe_with_set,
),
run_online(
"online-optimized",
online_payload,
parse_catalog_cached,
dedupe_with_set,
),
]
print_results(results)
def expected_list_comparisons(item_count):
return item_count * (item_count - 1) // 2
def project_growth(item_count, growth):
projected_items = item_count * growth
current_comparisons = expected_list_comparisons(item_count)
projected_comparisons = expected_list_comparisons(projected_items)
comparison_growth = (
projected_comparisons / current_comparisons
if current_comparisons
else 0.0
)
online_payload = build_payload(item_count, "ONLINE")
optimized = run_online(
"online-optimized",
online_payload,
parse_catalog_cached,
dedupe_with_set,
)
budget_status = (
"PASS"
if optimized["total_seconds"] <= STARTUP_BUDGET_SECONDS
else "REVIEW"
)
print("\n10x growth projection")
print("=" * 72)
print(f"Current catalog items: {item_count:,}")
print(f"Projected catalog items: {projected_items:,}")
print(f"Current naive comparisons: {current_comparisons:,}")
print(f"Projected naive comparisons: {projected_comparisons:,}")
print(f"Growth in naive comparison work: {comparison_growth:.1f}x")
print(
f"Current optimized startup: {optimized['total_seconds']:.4f}s "
f"against {STARTUP_BUDGET_SECONDS:.1f}s budget: {budget_status}"
)
print("\nRedesign controls")
print("1. Precompute a normalized, versioned catalog artifact before launch.")
print("2. Load only a minimal playable partition on the critical path.")
print("3. Cache the parsed snapshot and rebuild a set-backed key index by version.")
print("4. Initialize noncritical catalog partitions in the background after ready-to-play.")
print("5. Track ready-to-play and fully-ready budgets separately in regression tests.")
def print_help():
print("Usage:")
print(" python3 loading_lab.py baseline --items 6000")
print(" python3 loading_lab.py online-naive --items 6000")
print(" python3 loading_lab.py compare --items 6000")
print(" python3 loading_lab.py project --items 6000 --growth 10")
def parse_cli(argv):
if len(argv) < 2:
return "help", DEFAULT_ITEMS, 10
mode = argv[1]
if mode not in VALID_MODES:
raise SystemExit(f"Unknown mode: {mode}")
item_count = DEFAULT_ITEMS
growth = 10
index = 2
while index < len(argv):
option = argv[index]
if index + 1 >= len(argv):
raise SystemExit(f"Missing value for {option}")
value = argv[index + 1]
if option == "--items":
item_count = int(value)
elif option == "--growth":
growth = int(value)
else:
raise SystemExit(f"Unknown option: {option}")
index += 2
if item_count <= 0 or growth <= 0:
raise SystemExit("--items and --growth must be positive integers")
return mode, item_count, growth
def main():
mode, item_count, growth = parse_cli(sys.argv)
if mode == "help":
print_help()
elif mode == "baseline":
run_baseline(item_count)
elif mode == "online-naive":
run_online_naive(item_count)
elif mode == "compare":
run_comparison(item_count)
elif mode == "project":
project_growth(item_count, growth)
if __name__ == "__main__":
main()
Design the startup path
The optimized functions improve today's synchronous implementation. The architecture document adds precomputation, caching, partitioning, and background initialization so total catalog size stops controlling ready-to-play time.
- Create startup-redesign.md inside the gta-loading-lab folder with this first section:
# Startup Redesign for a 10x Online Catalog
## Context
The lab demonstrates two local CPU bottlenecks: repeated scans of the same serialized payload and list-based duplicate detection whose comparison count grows quadratically. Optimizing those functions fixes the immediate implementation, but keeping the full catalog on the synchronous startup path still couples ready-to-play latency to catalog growth.
## Decision
The client will treat ready-to-play and fully-ready as separate milestones.
1. The content pipeline will precompute a normalized catalog artifact with a schema version, catalog version, checksum, and partitions.
2. Startup will synchronously load only the bootstrap partition required for the first playable experience.
3. The client will cache the parsed bootstrap snapshot and construct a set-backed key index for membership checks.
4. Remaining catalog partitions will load after ready-to-play on background work with bounded concurrency.
5. A catalog version or checksum mismatch will invalidate the cache and fall back to the last valid bootstrap partition while a new snapshot is prepared.
## Performance Budget
- Lab ready-to-play target: optimized startup at or below 1.0 second on the learner's Mac.
- Fully-ready work is measured separately and must not block the ready-to-play milestone.
- Production budgets must be selected from product requirements and tested on representative low-end and high-end hardware.
How Does This Decision Change Startup?
- The bootstrap partition contains only the data required for the first playable experience.
- The cached snapshot avoids parsing unchanged bootstrap data on every launch.
- Version and checksum checks prevent stale cached data from being trusted.
- Save startup-redesign.md.
- Confirm the Visual Studio Code Explorer lists startup-redesign.md beneath loading_lab.py.
Document Missing from the Explorer?
Check that the file name is exactly startup-redesign.md. Make sure the file sits inside the open gta-loading-lab folder.
Help me find my startup redesign file.
- Append these scaling reasons and guardrails to startup-redesign.md below the production budget bullet:
## Why These Controls Scale
- Precomputation moves repeated transformation work out of the launch path.
- Partitioning makes critical work depend on the playable subset instead of total catalog size.
- Caching avoids parsing unchanged snapshots on every launch.
- Set-backed indexing prevents duplicate checking from becoming a nested scan.
- Background initialization keeps optional catalog data away from the user-visible startup boundary.
## Risks and Guardrails
- Stale cache: bind every cache entry to catalog version and checksum.
- Partial background failure: retain the bootstrap partition and retry optional partitions independently.
- Memory growth: release optional partitions that are not needed by the current session.
- Hidden regression: record ready-to-play and fully-ready timings separately on every benchmark run.
Why Add Guardrails?
Moving work into the background introduces cache freshness and partial-load risks. The guardrails preserve the playable partition while optional work retries independently.
Separate ready-to-play and fully-ready measurements keep deferred work visible. A faster first milestone cannot hide a slower complete initialization.
- Save startup-redesign.md.
- Confirm the document ends with the Risks and Guardrails section.
✔️ Awesome, I've got everything!
Your redesign now covers the immediate optimization and the architecture required for future growth.
ⓧ I'd like to double check the full code
# Startup Redesign for a 10x Online Catalog
## Context
The lab demonstrates two local CPU bottlenecks: repeated scans of the same serialized payload and list-based duplicate detection whose comparison count grows quadratically. Optimizing those functions fixes the immediate implementation, but keeping the full catalog on the synchronous startup path still couples ready-to-play latency to catalog growth.
## Decision
The client will treat ready-to-play and fully-ready as separate milestones.
1. The content pipeline will precompute a normalized catalog artifact with a schema version, catalog version, checksum, and partitions.
2. Startup will synchronously load only the bootstrap partition required for the first playable experience.
3. The client will cache the parsed bootstrap snapshot and construct a set-backed key index for membership checks.
4. Remaining catalog partitions will load after ready-to-play on background work with bounded concurrency.
5. A catalog version or checksum mismatch will invalidate the cache and fall back to the last valid bootstrap partition while a new snapshot is prepared.
## Performance Budget
- Lab ready-to-play target: optimized startup at or below 1.0 second on the learner's Mac.
- Fully-ready work is measured separately and must not block the ready-to-play milestone.
- Production budgets must be selected from product requirements and tested on representative low-end and high-end hardware.
## Why These Controls Scale
- Precomputation moves repeated transformation work out of the launch path.
- Partitioning makes critical work depend on the playable subset instead of total catalog size.
- Caching avoids parsing unchanged snapshots on every launch.
- Set-backed indexing prevents duplicate checking from becoming a nested scan.
- Background initialization keeps optional catalog data away from the user-visible startup boundary.
## Risks and Guardrails
- Stale cache: bind every cache entry to catalog version and checksum.
- Partial background failure: retain the bootstrap partition and retry optional partitions independently.
- Memory growth: release optional partitions that are not needed by the current session.
- Hidden regression: record ready-to-play and fully-ready timings separately on every benchmark run.
Enforce the startup budget
The one-second threshold is a local regression gate for this lab. A production team must select its budget from product requirements and measurements on representative hardware.
Before you run the final check, how much do you expect the naive comparison work to grow when the catalog grows by 10x?
- Run the final projection from the workspace Terminal with this command:
python3 loading_lab.py project --items 6000 --growth 10
What Does the Final Check Prove?
The command proves that the projected quadratic cost grows by about 100.0x while the optimized 6,000-item path remains safe to measure. The budget result records whether your current Mac stays within the local target.
You should see 60,000 projected items and approximately 100.0x naive comparison growth. Below the budget result, you should see five redesign controls covering precomputation, a playable partition, a cached index, background initialization, and separate measurements.
Final Output Missing a Section?
Check that every projection print statement remains indented inside project_growth(). Confirm that all five redesign-control lines appear after the budget output.
A REVIEW result still confirms that the regression gate works. It tells you the measured startup exceeded the lab's local target on this run.
Help me diagnose my final projection output.
Strong finish. Your lab now connects measured hot paths to a growth projection and a startup design that protects the playable milestone.
Secret mission
Prove the Faster Index Is Correct
Your optimized duplicate check is faster on a catalog of unique records. This mission injects repeated keys to test the behavior that duplicate detection must preserve. You will prove that the set-backed implementation keeps the same first-seen order while performing one membership check per input record.
Clean Up Your Resources
Clean Up Your Resources
Everything in this lab stays on your Mac with no ongoing costs. Choose whether to keep the workspace available, close it for now, or delete the local files.
Resources you used:
- Local gta-loading-lab workspace folder.
- The loading_lab.py startup simulator.
- The startup-redesign.md architecture record.
- The validate_duplicates.py correctness test.
Keep everything running
No action needed. Choose this if you want to rerun the investigation or refine your startup redesign.
- Keep the gta-loading-lab workspace on your Mac.
- Use loading_lab.py to repeat the baseline or optimized benchmarks.
- Use validate_duplicates.py to repeat the correctness check.
- Keep startup-redesign.md as your architecture decision record.
Pause - I'll come back to this later
Close your local tools while preserving every project file. The lab runs only when you execute a command.
- Save each open file in Visual Studio Code.
- Close the workspace Terminal after its current command finishes.
- Close Visual Studio Code.
- Resume later by reopening the gta-loading-lab folder in Visual Studio Code.
Delete - I don't want to use this again
Deleting a folder can feel risky. This command targets only gta-loading-lab.
Python 3.14.7 remains installed. Visual Studio Code also remains installed.
- Switch back to the workspace terminal from earlier.
- Move to the parent folder and remove the local lab by running these commands:
cd ..
rm -rf gta-loading-lab
What do these commands do?
- The cd .. command moves Terminal to the folder containing gta-loading-lab.
- The rm -rf gta-loading-lab command removes the workspace folder plus every file inside it.
- Confirm the workspace folder is gone by listing the contents of its former parent folder:
ls
What should I see?
You should no longer see gta-loading-lab in the output. Cleanup complete. The lab files are no longer stored on your Mac.
Still see the folder?
- Check that Terminal is showing the folder that previously contained gta-loading-lab.
- Remove gta-loading-lab with your Mac's file browser if Terminal reports a permissions problem.
- Help me safely remove my local GTA loading lab folder.
Nice Work!
Nice Work!
You did it! Your local performance investigation lab now measures a simulated startup critical path from its baseline through 10x catalog growth.
You've learned how to:
- Built a story-versus-online benchmark that makes the slower online startup path visible through elapsed time.
- Turned profiler evidence into two targeted fixes: cached payload-length work plus set-backed indexing.
- Created a 10x startup redesign using precomputation, bootstrap partitioning, caching, background initialization, plus separate ready-to-play and fully-ready measurements.
- Secret Mission: Proved the faster set-backed implementation preserves first-seen behavior across six records containing duplicate CAR_001 plus CAR_002 keys.
Ready to quiz yourself?