Build a Search and AI Mode Simulator
Build a browser simulator for distributed search, AI fan-out, and fallback.
Introduction
30 Second Summary
Search results often appear before you have time to wonder how much work happened behind the box. One slow part can delay everything you see.
In this project, you will build a local Google Search and AI Mode simulator that traces each query from request to result. You will use measured runs to defend how the design handles latency under failure.
What You'll Build
Your browser turns a query into a stage-by-stage timeline that exposes whether Classic Search or AI Mode meets the lab's latency target.
By the end of this project, you'll have:
- A Classic Search trace that shows every stage from request gateway through presentation. You can see total latency plus the SLO result after each run.
- A distributed retrieval experiment where changing sharding, replicas, cache behavior, or failure scenarios reveals tail latency, partial failures, or stale reads.
- An evidence-based design defense that compares sequential with concurrent query fan-out using the same simulation seed. Its measured outcome explains when deadlines, partial responses, or classic fallback protect the SLO.
- Secret Mission: Tune the simulator for a synthetic traffic spike. You will change one control to defend its latency, availability, or freshness tradeoff with measured evidence.
What do I need before starting?
You need comfort with distributed systems plus basic JavaScript. No package manager, cloud account, API key, or AI Mode access is required.
Before We Start
Before any hands-on work begins, this checkpoint defines the Search Architecture Lab as a local synthetic simulator that needs no Google account, API key, package manager, or cloud service. It distinguishes documented Google Search and AI Mode behavior from illustrative assumptions about sharding, replicas, routing, caching, deadlines, and fallback.
Set Up the Static Lab
A distributed-search simulator needs a fast feedback loop. A local static page lets you test each change without backend plumbing or cloud setup.
You will use Visual Studio Code to edit HTML, CSS, and JavaScript. Safari will display the lab directly from your Mac.
In this step, get ready to:
- Prepare Visual Studio Code 1.137 or higher for the project.
- Create the search-architecture-lab folder with three starter files.
- Open the local page in Safari to confirm the heading renders.
Check Visual Studio Code
The project uses Visual Studio Code 1.137 as its pinned editor. Checking the installed release prevents interface differences from getting in your way.
- Press Cmd+Space to open Spotlight.
- Type Visual Studio Code into Spotlight.
- Press Enter when Visual Studio Code appears.
- Select Code in the macOS menu bar.
- Select About Visual Studio Code to check the installed version.
Your result now falls into one of these three states.
✔️ I see version 1.137 or higher
Your editor is ready for the lab. You can close the version window and continue.
ⓧ I see an older version
The installed release needs an update before you create the workspace. Installing the current release gives you the interface used throughout this project.
- Visit the official Visual Studio Code macOS setup page.
- Download the macOS .dmg.
- Open the downloaded .dmg.
- Drag Visual Studio Code.app to Applications.
- Complete the replacement if macOS asks.
- Launch Visual Studio Code from Applications.
- Select Code in the macOS menu bar.
- Select About Visual Studio Code to confirm version 1.137 or higher.
The updated version number confirms that your editor matches the project baseline.
ⓧ Visual Studio Code is not installed
Spotlight cannot launch an app that has not been installed. The macOS installer adds Visual Studio Code to your Applications folder.
- Visit the official Visual Studio Code macOS setup page.
- Download the macOS .dmg.
- Open the downloaded .dmg.
- Drag Visual Studio Code.app to Applications.
- Launch Visual Studio Code from Applications.
- Select Code in the macOS menu bar.
- Select About Visual Studio Code to confirm version 1.137 or higher.
The editor is now installed and ready to hold your local project.
Installation Did Not Complete?
- Confirm that the downloaded file uses the .dmg extension.
- Confirm that Visual Studio Code.app is inside Applications.
Ask for targeted help with troubleshooting the Visual Studio Code installation.
Create the project files
Each starter file owns one part of the browser experience. index.html defines the page while styles.css controls its appearance.
The app.js file will hold the simulator logic. Keeping these responsibilities separate makes later experiments easier to trace.
Why a Static Lab?
A static lab runs directly in the browser without a package manager or local server. This keeps your attention on latency paths and failure policies.
You still get a working interface with measurable behavior. Every later system-design decision stays visible in one browser page.
- Close the version window.
- Select File in the Visual Studio Code menu bar.
- Select Open Folder....
- Select Desktop in the folder chooser.
- Create a folder named search-architecture-lab.
- Select the search-architecture-lab folder.
- Select Open.
The Explorer sidebar now shows the empty search-architecture-lab folder. This confirms that new files will land in the intended workspace.
- Create index.html by selecting New File... in the Explorer.
- Create styles.css by selecting New File... in the Explorer.
- Create app.js by selecting New File... in the Explorer.
Good progress. The Explorer now lists all three files that make up the static lab.
Build and open the starter page
The starter document connects the page to its stylesheet and script. The defer attribute lets the browser finish reading the document before it runs the script.
- Add the starter document to index.html by copying the code below:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Search Architecture Lab</title>
<link rel="stylesheet" href="styles.css">
<script src="app.js" defer></script>
</head>
<body>
<h1>Search Architecture Lab</h1>
</body>
</html>
What Does This HTML Do?
- The document metadata gives the browser a valid page structure.
- The stylesheet link loads the visual rules from styles.css.
- The deferred script loads the future simulator logic from app.js.
- The heading creates an immediate browser checkpoint.
- Save index.html.
- Select View in the Visual Studio Code menu bar.
- Select Terminal to open the integrated Terminal.
- Open the starter page from the workspace by running this command:
open index.html
What Does This Command Do?
The open command asks macOS to open index.html with its browser application. The file stays local on your Mac.
Your first visible checkpoint is live. Safari shows the heading “Search Architecture Lab.”
Heading Missing?
- Confirm that the integrated Terminal starts inside search-architecture-lab.
- Confirm that the heading exists inside the body element in index.html.
- Save index.html before reopening it.
Ask for targeted help with diagnosing the missing HTML heading.
The stylesheet establishes a clean base for the interface. It removes the browser's body margin and uses the Mac system font stack.
- Add the base page styles to styles.css by copying the code below:
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
What Does This CSS Do?
- The zero body margin removes the browser's default outer page gap.
- The font stack uses the system interface font available on the learner's device.
- Save styles.css.
- Return to Safari.
- Refresh the page.
The heading now sits against the left edge and uses the system font. This visible change confirms that index.html loads styles.css.
Style Did Not Change?
- Confirm that the stylesheet file is named styles.css.
- Confirm that the stylesheet link in index.html uses the same filename.
- Save styles.css before refreshing Safari.
Ask for targeted help with diagnosing the missing CSS styles.
The script begins with strict mode enabled. This gives later simulator code a stricter JavaScript execution baseline.
- Add the strict-mode directive to app.js by copying the code below:
"use strict";
What Does This JavaScript Do?
The directive enables stricter JavaScript behavior for this file. Later simulation functions build on this baseline.
- Save app.js.
Before you run the final check, what heading do you expect Safari to display?
- Verify the completed starter page by running this command in the integrated Terminal:
open index.html
What Does This Final Check Prove?
The command reopens the saved local document from the workspace. A rendered heading confirms that the starter page remains valid after connecting its stylesheet and script.
Safari shows “Search Architecture Lab.” Your three-file static lab now opens directly from the workspace.
Page Did Not Reopen?
- Confirm that the Terminal prompt is inside search-architecture-lab.
- Confirm that index.html appears in the Explorer.
- Confirm that all three editor tabs are saved.
Ask for targeted help with reopening the local starter page.
✔️ Awesome, I've got everything!
Your three starter files are saved. Safari can now open the local lab from the project workspace.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Search Architecture Lab</title>
<link rel="stylesheet" href="styles.css">
<script src="app.js" defer></script>
</head>
<body>
<h1>Search Architecture Lab</h1>
</body>
</html>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
"use strict";
Your dependency-free workspace is ready. Next, you will turn this starter page into a visible classic-query architecture and latency trace.
Trace a Classic Search Query
Your starter page in Visual Studio Code already opens in Safari. It now needs to show how a query moves from public search behavior to a measurable result.
A visible classic-query path establishes the foundation before AI fan-out adds more latency or failure surfaces. You will map crawling, indexing, retrieval, ranking, and aggregation against a learner-defined latency SLO.
In this step, get ready to:
- Build a three-lane architecture diagram with a crawlable-to-grounded dependency chain.
- Add experiment controls, metric cards, a timeline table, and a design-defense area.
- Run a classic Search simulation against an explicit latency SLO.
Build the architecture diagram
The diagram separates documented Google Search behavior from the simulator's illustrative infrastructure. This boundary lets you reason about distributed-system tradeoffs without presenting private implementation guesses as facts.
- In index.html, select the existing <h1> element.
- Replace it with the first architecture chunk below:
<header class="hero">
<p class="eyebrow">Distributed systems design lab</p>
<h1>Search Architecture Lab</h1>
<p>Measure classic retrieval and AI query fan-out without claiming access to Google's private implementation.</p>
</header>
<main>
<section class="panel">
<div class="section-heading">
<div>
<p class="eyebrow">Architecture</p>
<h2>Public behavior mapped to an illustrative design</h2>
</div>
<span class="badge">Not a Google internal diagram</span>
</div>
<div class="architecture" aria-label="Search architecture diagram">
<div class="lane">
<h3>Ingestion</h3>
<div class="flow">
<span class="node documented">URL discovery</span><span class="arrow">→</span>
<span class="node documented">Crawler and render</span><span class="arrow">→</span>
<span class="node documented">Canonicalize and index</span><span class="arrow">→</span>
<span class="node assumed">Replica propagation</span>
</div>
</div>
What Does This Markup Do?
The header states the simulator's scope. The ingestion lane distinguishes documented discovery and indexing behavior from the illustrative replica-propagation model.
- Place the next architecture chunk directly below the first chunk:
<div class="lane">
<h3>Classic serving</h3>
<div class="flow">
<span class="node assumed">Request gateway</span><span class="arrow">→</span>
<span class="node assumed">Query router and cache</span><span class="arrow">→</span>
<span class="node assumed">Index shards and replicas</span><span class="arrow">→</span>
<span class="node assumed">Ranking and aggregation</span><span class="arrow">→</span>
<span class="node documented">Search results</span>
</div>
</div>
<div class="lane">
<h3>AI Mode serving</h3>
<div class="flow">
<span class="node documented">Query fan-out</span><span class="arrow">→</span>
<span class="node assumed">Parallel retrieval branches</span><span class="arrow">→</span>
<span class="node documented">Grounding</span><span class="arrow">→</span>
<span class="node documented">Response and links</span><span class="arrow">→</span>
<span class="node assumed">Classic fallback</span>
</div>
</div>
</div>
<div class="dependency-chain">
<span>Crawlable</span><span>→</span><span>Indexed</span><span>→</span><span>Retrieved</span><span>→</span><span>Ranked</span><span>→</span><span>Grounded and cited</span>
</div>
</section>
</main>
How Are the Paths Separated?
The classic lane follows retrieval through ranked results. The AI Mode lane extends retrieval into grounding while retaining a classic fallback.
The dependency chain makes eligibility visible. A page must become crawlable and indexed before it can support a grounded response.
- Save index.html.
- Switch back to Safari.
- Refresh the page.
You should see the lab title followed by Ingestion, Classic serving, and AI Mode serving lanes. You should also see the crawlable-to-grounded chain below them.
Diagram Content Missing?
Check that both chunks sit between the existing <body> tags. Confirm that the closing </main> appears before </body>.
Ask for help with checking the architecture markup.
The architecture is now visible. Styling will turn its labels into distinct documented and illustrative paths.
- In styles.css, select the existing stylesheet.
- Replace it with the page foundation below:
:root {
color-scheme: light;
--ink: #172033;
--muted: #5d6678;
--panel: #ffffff;
--line: #d9dfeb;
--blue: #2457d6;
--blue-soft: #eaf0ff;
--green: #087a55;
--green-soft: #e4f7ef;
--amber: #9a5b00;
--amber-soft: #fff3d8;
--red: #b42318;
--red-soft: #feeceb;
--background: #f4f7fb;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--background);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
}
What Does This CSS Set Up?
The shared color values give documented behavior, assumptions, warnings, and failures consistent visual roles. Border-box sizing keeps each panel within its intended width.
- Save styles.css.
- Refresh Safari.
You should see a pale page background with dark text. The browser now applies the lab's shared color system.
Colors Not Applied?
Confirm that index.html still links styles.css in the document head. Check that the CSS variable names match the code exactly.
Ask for help with diagnosing the stylesheet link.
- Place the hero and panel styles below the existing body rule:
.hero {
padding: 48px max(24px, calc((100vw - 1180px) / 2));
color: white;
background: linear-gradient(120deg, #14213d, #2457d6);
}
.hero h1 {
margin: 4px 0 8px;
font-size: clamp(2rem, 5vw, 3.5rem);
}
.hero p:last-child {
max-width: 760px;
margin-bottom: 0;
}
main {
width: min(1180px, calc(100% - 32px));
margin: 28px auto 64px;
}
.panel {
margin-bottom: 20px;
padding: 24px;
border: 1px solid var(--line);
border-radius: 18px;
background: var(--panel);
box-shadow: 0 10px 30px rgba(23, 32, 51, 0.06);
}
How Does the Layout Take Shape?
The hero gives the simulator a clear title area. The centered main column turns each architecture section into a bounded review panel.
- Save styles.css.
- Refresh Safari.
You should see a blue gradient header. The architecture content should sit inside a white rounded panel.
Panel Still Looks Like Plain Text?
Check that the architecture section uses class="panel". Confirm that the new rules appear after the body rule in styles.css.
Ask for help with matching the panel class.
- Add the section-heading styles below the panel styles:
.section-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
margin-bottom: 18px;
}
.section-heading h2,
.lane h3 {
margin: 0;
}
.eyebrow {
margin: 0 0 4px;
color: var(--blue);
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.badge {
padding: 6px 10px;
border-radius: 999px;
color: var(--amber);
background: var(--amber-soft);
font-size: 0.8rem;
font-weight: 700;
}
Why Highlight the Boundary?
The badge keeps the scope warning beside the architecture title. This makes the distinction between documented behavior and illustrative design visible during every demo.
- Save styles.css.
- Refresh Safari.
You should see the Architecture eyebrow in blue. The scope warning should appear as an amber pill.
Badge Not Beside the Heading?
Confirm that the title and badge are both inside class="section-heading". Check that the badge uses class="badge".
Ask for help with fixing the section heading layout.
- Add the lane layout below the badge styles:
.architecture {
display: grid;
gap: 14px;
}
.lane {
padding: 16px;
border: 1px solid var(--line);
border-radius: 14px;
background: #fbfcff;
}
.flow,
.dependency-chain {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
overflow-x: auto;
padding-bottom: 4px;
}
.node,
.dependency-chain span:nth-child(odd) {
min-width: max-content;
padding: 10px 12px;
border-radius: 10px;
font-size: 0.88rem;
font-weight: 700;
}
How Do the Lanes Stay Readable?
Each lane becomes a separate bordered row. Horizontal overflow preserves the stage order on narrow screens without squeezing labels into unreadable columns.
- Save styles.css.
- Refresh Safari.
You should see three bordered architecture lanes. Each stage should appear as a compact rounded node.
Nodes Wrapping Into Columns?
Check that each lane contains a class="flow" container. Confirm that min-width: max-content remains inside the node rule.
Ask for help with repairing the lane flow.
- Add the documented and assumed node styles below the node rule:
.node.documented {
color: var(--green);
background: var(--green-soft);
}
.node.assumed {
color: var(--blue);
background: var(--blue-soft);
}
.arrow {
color: var(--muted);
font-weight: 800;
}
.dependency-chain {
margin-top: 18px;
padding: 14px;
border-radius: 12px;
background: #172033;
color: white;
}
.dependency-chain span:nth-child(odd) {
background: rgba(255, 255, 255, 0.12);
}
What Do the Colors Mean?
Green nodes represent behavior documented publicly. Blue nodes represent the simulator's assumed infrastructure choices.
The dark dependency chain highlights the sequence required before a page can support grounding and citations.
- Save styles.css.
- Refresh Safari.
You should see green documented nodes and blue illustrative nodes. The dependency chain should appear on a dark background.
Every Node Has the Same Color?
Confirm that documented nodes use class="node documented". Confirm that illustrative nodes use class="node assumed".
Ask for help with checking the node classes.
Add the experiment and evidence interface
A system-design diagram becomes testable when it exposes a workload and a measurable target. The controls will define the query path while the metrics and timeline reveal its result.
- In index.html, find the closing </main> tag.
- Place the first experiment chunk directly above it:
<section class="panel controls-panel">
<div class="section-heading">
<div>
<p class="eyebrow">Experiment</p>
<h2>Set the workload and failure model</h2>
</div>
</div>
<div class="controls">
<label>Scenario query
<input id="query" value="compare sleep tracking devices for travel">
</label>
<label>Mode
<select id="mode">
<option value="classic">Classic Search</option>
<option value="ai">AI Mode</option>
</select>
</label>
<label>Fan-out strategy
<select id="fanoutStrategy">
<option value="sequential">Sequential</option>
<option value="concurrent">Concurrent</option>
</select>
</label>
<label>Latency SLO in ms
<input id="slo" type="number" min="400" max="2000" step="50" value="700">
</label>
<label>Index shards
<input id="shardCount" type="number" min="2" max="32" value="8">
</label>
What Does the Workload Capture?
The query and mode define what the simulator runs. The SLO sets the maximum acceptable end-to-end latency for the lab.
The extra controls prepare the same interface for later experiments. This step uses the classic mode and SLO directly.
- Continue the controls directly below the shard field:
<label>Replicas per shard
<input id="replicaCount" type="number" min="1" max="5" value="3">
</label>
<label>Cache hit rate in percent
<input id="cacheRate" type="number" min="0" max="100" value="20">
</label>
<label>Replica failure rate in percent
<input id="failureRate" type="number" min="0" max="80" value="0">
</label>
<label>AI fan-out branches
<input id="fanoutCount" type="number" min="1" max="12" value="6">
</label>
<label>Fault scenario
<select id="faultScenario">
<option value="normal">Normal</option>
<option value="partial-outage">Two fan-out branches fail</option>
<option value="quorum-outage">Fan-out quorum lost</option>
<option value="replica-lag">Replica propagation lag</option>
</select>
</label>
</div>
<div class="actions">
<button id="runButton">Run simulation</button>
<button id="compareButton" class="secondary">Compare fan-out</button>
</div>
<p class="assumption">The 700 ms default is a lab SLO, not a published Google Search SLO. All component latencies are synthetic.</p>
</section>
Why State the Assumption?
The default target makes the experiment measurable. The assumption text prevents the synthetic 700 ms value from being mistaken for a published production SLO.
- Save index.html.
- Refresh Safari.
You should see workload controls followed by Run simulation and Compare fan-out buttons. The assumption below them should identify the latency values as synthetic.
Controls Missing From the Page?
Check that the controls section appears before the final </main> tag. Confirm that every label closes after its input or select element.
Ask for help with checking the controls markup.
- Place the metric cards above the closing </main> tag:
<section class="metrics" aria-label="Simulation metrics">
<article><span>Outcome</span><strong id="metricOutcome">Not run</strong></article>
<article><span>Total latency</span><strong id="metricLatency">0 ms</strong></article>
<article><span>SLO</span><strong id="metricSlo">Not evaluated</strong></article>
<article><span>Cache</span><strong id="metricCache">No data</strong></article>
<article><span>AI sources</span><strong id="metricSources">0</strong></article>
<article><span>Freshness</span><strong id="metricFreshness">No scenario</strong></article>
</section>
<section class="panel">
<div class="section-heading">
<div>
<p class="eyebrow">Evidence</p>
<h2>Fan-out comparison</h2>
</div>
</div>
<p id="comparisonNote">Use the same seed to compare sequential and concurrent execution fairly.</p>
<div class="table-wrap">
<table>
<thead><tr><th>Strategy</th><th>Latency</th><th>Outcome</th><th>Sources</th><th>SLO</th></tr></thead>
<tbody id="comparisonBody"><tr><td colspan="5">Select Compare fan-out.</td></tr></tbody>
</table>
</div>
</section>
What Evidence Will Appear Here?
The metric cards summarize each run. The comparison table reserves a shared evidence area for sequential and concurrent AI experiments in a later step.
- Save index.html.
- Refresh Safari.
You should see six metric cards with their initial values. A fan-out comparison table should appear beneath them.
Metric Cards Not Visible?
Confirm that the metrics section appears after the controls section. Check that each strong element keeps its exact ID.
Ask for help with finding a missing metric ID.
- Place the timeline and defense sections above the closing </main> tag:
<section class="panel">
<div class="section-heading">
<div>
<p class="eyebrow">Critical path</p>
<h2>Stage timeline</h2>
</div>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Stage</th><th>Start</th><th>End</th><th>Duration</th><th>Status</th></tr></thead>
<tbody id="timelineBody"></tbody>
</table>
</div>
</section>
<section class="panel">
<div class="section-heading">
<div>
<p class="eyebrow">Design review</p>
<h2>System-design defense</h2>
</div>
</div>
<ul id="defenseList"></ul>
</section>
Why Show a Timeline and Defense?
The timeline exposes the critical path stage by stage. The defense converts the measured result into a concise architectural argument.
- Save index.html.
- Refresh Safari.
You should see an empty stage timeline followed by the System-design defense panel. JavaScript will populate both areas in the final substep.
Timeline Table Not Showing?
Confirm that the table body uses id="timelineBody". Confirm that the defense list uses id="defenseList".
Ask for help with checking the evidence IDs.
The interface now contains every surface needed to run and inspect a query. The remaining styles turn the controls, metrics, and tables into a responsive dashboard.
- Add the controls layout below the dependency-chain styles in styles.css.
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 14px;
}
label {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 0.85rem;
font-weight: 700;
}
input,
select,
button {
min-height: 42px;
border-radius: 9px;
font: inherit;
}
input,
select {
width: 100%;
border: 1px solid var(--line);
padding: 8px 10px;
color: var(--ink);
background: white;
}
How Are the Controls Organized?
The responsive grid gives each control enough room for its label and value. Shared form styles keep inputs, selects, and buttons aligned.
- Save styles.css.
- Refresh Safari.
You should see the controls arranged in responsive columns. Each input and select should have a consistent height.
Controls Still Form One Long Column?
Check that the label elements sit inside class="controls". Confirm that the grid-template rule remains inside the controls selector.
Ask for help with repairing the controls grid.
- Add the action styles below the form controls:
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
}
button {
border: 0;
padding: 0 18px;
color: white;
background: var(--blue);
cursor: pointer;
font-weight: 800;
}
button.secondary {
color: var(--blue);
background: var(--blue-soft);
}
.assumption,
#comparisonNote {
color: var(--muted);
font-size: 0.88rem;
}
How Are Primary Actions Signaled?
The blue Run simulation button carries the main action. The softer comparison button remains visible without competing with the classic-query run.
- Save styles.css.
- Refresh Safari.
You should see a solid blue Run simulation button. Compare fan-out should use a pale blue background.
Buttons Look Identical?
Confirm that the comparison button uses class="secondary". Check that the secondary rule appears after the main button rule.
Ask for help with fixing the secondary button.
- Add the metric-card styles below the action styles:
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.metrics article {
min-height: 110px;
padding: 16px;
border: 1px solid var(--line);
border-radius: 14px;
background: white;
}
.metrics span,
.metrics strong {
display: block;
}
.metrics span {
color: var(--muted);
font-size: 0.82rem;
font-weight: 700;
}
.metrics strong {
margin-top: 10px;
font-size: 1.08rem;
}
Why Use Metric Cards?
Each card isolates one observable result. The layout lets you scan latency, SLO status, cache behavior, source count, and freshness without reading the full timeline.
- Save styles.css.
- Refresh Safari.
You should see six white metric cards arranged across the page. Each value should sit below a smaller label.
Metrics Have No Card Styling?
Confirm that each metric is an article inside class="metrics". Check the selector punctuation around .metrics article.
Ask for help with diagnosing the metric selectors.
- Add the table styles below the metric styles:
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 11px 10px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
th {
color: var(--muted);
font-size: 0.78rem;
text-transform: uppercase;
}
How Do the Tables Handle Small Screens?
The wrapper allows horizontal scrolling when the columns exceed the available width. Collapsed borders keep each timeline row visually connected.
- Save styles.css.
- Refresh Safari.
You should see uppercase table headings with horizontal row dividers. The comparison table should remain contained inside its panel.
Table Extending Past the Panel?
Confirm that each table sits inside class="table-wrap". Check that overflow is set on the wrapper instead of the table.
Ask for help with containing the tables.
- Add the status and defense styles below the table styles:
.status-good {
color: var(--green);
font-weight: 800;
}
.status-warn {
color: var(--amber);
font-weight: 800;
}
.status-bad {
color: var(--red);
font-weight: 800;
}
#defenseList {
margin-bottom: 0;
padding-left: 22px;
}
#defenseList li {
margin: 8px 0;
}
What Will the Status Colors Show?
Green identifies a successful SLO result. Amber and red are available for degraded or failed outcomes as later scenarios add more failure states.
- Save styles.css.
- Refresh Safari.
The defense list should have readable spacing. The status colors become visible after the simulation writes a result.
Defense List Touching the Panel Edge?
Confirm that the list uses id="defenseList". Check that its left padding remains inside the defense-list rule.
Ask for help with fixing the defense spacing.
- Finish the responsive stylesheet with the mobile rules below:
@media (max-width: 700px) {
.hero {
padding-top: 34px;
padding-bottom: 34px;
}
.panel {
padding: 18px;
}
.section-heading {
display: grid;
}
}
What Changes on Narrow Screens?
The mobile rule reduces unused space. It also stacks section headings so the scope badge does not compete with the title.
- Save styles.css.
- Resize the Safari window to a narrow width.
You should see the section headings stack vertically. The architecture lanes and tables should remain available through horizontal scrolling.
Mobile Layout Not Changing?
Confirm that the media rule sits at the end of styles.css. Check that its closing braces match the code exactly.
Ask for help with checking the media query.
Simulate and render the classic query
A deterministic random-number generator gives each run a seed that can be recorded in the defense. The classic simulation then advances a cursor through gateway, understanding, retrieval, ranking, aggregation, and presentation stages.
- In app.js, place the helper setup below "use strict";.
const byId = (id) => document.getElementById(id);
let runCounter = 0;
function createRng(seed) {
let value = seed >>> 0;
return function nextRandom() {
value = (value * 1664525 + 1013904223) % 4294967296;
return value / 4294967296;
};
}
function randomInt(rng, min, max) {
return Math.floor(rng() * (max - min + 1)) + min;
}
function nextSeed() {
runCounter += 1;
return Math.floor(Math.random() * 1000000000) + runCounter;
}
What Do the Helpers Provide?
The DOM helper retrieves interface elements by ID. The seeded generator produces repeatable pseudo-random values from a recorded seed.
The run counter helps each new simulation receive a fresh seed. This supports fair comparisons later.
- Save app.js.
- Refresh Safari.
The page should reload without losing the architecture or controls. The simulation remains inactive until the remaining functions are added.
Page Blank After Adding Helpers?
Check the braces inside createRng and nextSeed. Confirm that app.js remains deferred in the document head.
Ask for help with diagnosing the helper syntax.
- Add the configuration reader below nextSeed().
function getConfig() {
return {
query: byId("query").value.trim() || "example query",
mode: byId("mode").value,
strategy: byId("fanoutStrategy").value,
slo: Number(byId("slo").value),
shards: Number(byId("shardCount").value),
replicas: Number(byId("replicaCount").value),
cacheRate: Number(byId("cacheRate").value),
failureRate: Number(byId("failureRate").value),
fanoutCount: Number(byId("fanoutCount").value),
faultScenario: byId("faultScenario").value
};
}
What Does the Configuration Reader Do?
The function reads every experiment control into one object. Numeric fields are converted from form text into numbers before simulation logic uses them.
- Save app.js.
- Refresh Safari.
The page should remain visible with the default query and 700 SLO intact. This confirms that every referenced control ID exists.
Seeing a Missing-Element Error?
Compare each ID in getConfig() with its matching input or select in index.html. A single spelling difference makes the requested element unavailable.
Ask for help with matching the control IDs.
The classic path uses a nested stage helper so every duration advances the same end-to-end cursor. The first half creates the timeline and records its stages.
- Add the first half of simulateClassicQuery() below getConfig().
function simulateClassicQuery(config, rng) {
const timeline = [];
let cursor = 0;
function addStage(stage, duration, status) {
const start = cursor;
cursor += duration;
timeline.push({ stage, start, end: cursor, duration, status });
}
addStage("Request gateway", 10, "Accepted");
addStage("Query understanding", 24, "Intent parsed");
addStage("Retrieval", randomInt(rng, 70, 120), "Index candidates retrieved");
addStage("Ranking", 28, "Candidate scores merged");
addStage("Aggregation", 16, "Result page assembled");
addStage("Presentation", 12, "Visible to user");
How Is Latency Accumulated?
Each stage begins where the previous stage ended. Retrieval receives a seeded duration while the surrounding stages use fixed synthetic values.
- Complete simulateClassicQuery() with the result object below:
return {
mode: "classic",
outcome: "Classic results",
latency: cursor,
sloMet: cursor <= config.slo,
cacheLabel: "Not modeled yet",
sources: 0,
freshness: "Not modeled yet",
timeline,
strategy: "Classic"
};
}
What Does the Result Capture?
The returned object contains the total cursor value and the full stage timeline. It also evaluates the total against the configured SLO.
Cache and freshness remain explicitly unmodeled in this version. The next retrieval step introduces those tradeoffs.
- Save app.js.
- Refresh Safari.
The page should reload without a syntax error. The visible controls remain ready for the rendering functions.
Classic Function Has a Syntax Error?
Confirm that the result object sits inside simulateClassicQuery(). Check that the function ends with one closing brace after the returned object.
Ask for help with checking the simulation function.
- Add the design-defense builder below simulateClassicQuery().
function buildDefense(result, config) {
return [
`Query: “${config.query}”. Simulation seed: ${result.seed}.`,
`SLO: the lab target is ${config.slo} ms. This run finished in ${result.latency} ms and ${result.sloMet ? "met" : "missed"} it.`,
"Scope boundary: public Google behavior is separated from synthetic component boundaries and synthetic latency assumptions."
];
}
What Makes the Defense Evidence-Based?
The defense records the query, seed, target, measured latency, and SLO result. It closes by restating the boundary between public behavior and synthetic assumptions.
- Save app.js.
- Refresh Safari.
The page should remain stable. The defense area stays empty until a result is rendered.
Template Text Causing an Error?
Confirm that the first two defense entries use backticks. Check that each placeholder keeps its opening ${ and closing brace.
Ask for help with repairing the defense templates.
- Add the timeline renderer below buildDefense().
function renderTimeline(timeline) {
byId("timelineBody").innerHTML = timeline.map((row) => `
<tr>
<td>${row.stage}</td>
<td>${row.start} ms</td>
<td>${row.end} ms</td>
<td>${row.duration} ms</td>
<td>${row.status}</td>
</tr>
`).join("");
}
How Is the Timeline Rendered?
The renderer converts every stage object into one table row. Joining the rows produces a single HTML string for the timeline body.
- Save app.js.
- Refresh Safari.
The timeline should remain empty without displaying an error. It now has a renderer ready to receive stage data.
Timeline Markup Appearing as Text?
Confirm that the renderer assigns to innerHTML. Check that the table row markup remains inside backticks.
Ask for help with fixing the timeline renderer.
- Add the defense renderer below renderTimeline().
function renderDefense(items) {
const list = byId("defenseList");
list.innerHTML = "";
items.forEach((item) => {
const entry = document.createElement("li");
entry.textContent = item;
list.appendChild(entry);
});
}
Why Build List Items Safely?
The renderer clears the previous defense before adding the current one. Assigning each sentence through textContent keeps the measured text separate from HTML markup.
- Save app.js.
- Refresh Safari.
The defense panel should remain empty without an error. Its list can now receive the generated design statements.
Defense Renderer Failing?
Confirm that defenseList matches the ID in index.html. Check that appendChild receives the new list item.
Ask for help with diagnosing the defense renderer.
- Add the result renderer below renderDefense().
function renderResult(result, config) {
byId("metricOutcome").textContent = result.outcome;
byId("metricLatency").textContent = `${result.latency} ms`;
byId("metricSlo").textContent = result.sloMet ? `Met (${config.slo} ms)` : `Missed (${config.slo} ms)`;
byId("metricCache").textContent = result.cacheLabel;
byId("metricSources").textContent = String(result.sources);
byId("metricFreshness").textContent = result.freshness;
renderTimeline(result.timeline);
renderDefense(buildDefense(result, config));
}
What Does the Result Renderer Update?
The renderer writes the summary metrics first. It then delegates the detailed timeline and defense to their dedicated renderers.
- Save app.js.
- Refresh Safari.
The initial metric values should remain visible. The interface now has every renderer required for a classic result.
A Metric Disappeared?
Compare every metric ID in renderResult() with the matching strong element in index.html.
Ask for help with finding the mismatched metric.
The final function joins configuration, simulation, and rendering into one run. The click listener gives the primary button the same behavior.
- Add the run function and event listener below renderResult().
function runCurrent() {
const config = getConfig();
const seed = nextSeed();
const result = simulateClassicQuery(config, createRng(seed));
result.seed = seed;
renderResult(result, config);
}
byId("runButton").addEventListener("click", runCurrent);
runCurrent();
How Does a Run Start?
The function captures the current controls and generates a new seed. It simulates the classic path before passing the result to the interface.
The listener runs the same flow after every button click. The final call also produces an initial result when Safari loads the page.
Before you save, do you expect the total to stay within the default 700 ms SLO?
- Save app.js.
- Switch back to Safari.
- Refresh the page.
You should see Classic results in the Outcome card. The Total latency card should show a measured value while the SLO card reports the result against 700 ms.
Simulation Did Not Run?
Confirm that the button uses id="runButton". Check Safari's developer console for the first missing function or element named by the error.
Ask for help with diagnosing the classic run.
Your first automatic run proves the full path works. Now test the exact classic-query verification from the controls.
Before you run it, which stages do you expect to appear between the request gateway and presentation?
- Set Mode to Classic Search.
- Select Run simulation.
You should see Request gateway, Query understanding, Retrieval, Ranking, Aggregation, and Presentation in the timeline. The page should also show a total latency, an SLO result, and a three-line system-design defense.
Timeline or Defense Still Empty?
Confirm that the click listener appears after every function declaration. Check that runCurrent() is called once at the end of app.js.
Ask for help with tracing the render flow.
That is the classic path working end to end. Your simulator now turns a search request into measurable latency and an evidence-based design defense.
✔️ Awesome, I've got everything!
Your three files are saved. Safari shows the architecture, controls, metrics, timeline, and generated defense.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Search Architecture Lab</title>
<link rel="stylesheet" href="styles.css">
<script src="app.js" defer></script>
</head>
<body>
<header class="hero">
<p class="eyebrow">Distributed systems design lab</p>
<h1>Search Architecture Lab</h1>
<p>Measure classic retrieval and AI query fan-out without claiming access to Google's private implementation.</p>
</header>
<main>
<section class="panel">
<div class="section-heading">
<div>
<p class="eyebrow">Architecture</p>
<h2>Public behavior mapped to an illustrative design</h2>
</div>
<span class="badge">Not a Google internal diagram</span>
</div>
<div class="architecture" aria-label="Search architecture diagram">
<div class="lane">
<h3>Ingestion</h3>
<div class="flow">
<span class="node documented">URL discovery</span><span class="arrow">→</span>
<span class="node documented">Crawler and render</span><span class="arrow">→</span>
<span class="node documented">Canonicalize and index</span><span class="arrow">→</span>
<span class="node assumed">Replica propagation</span>
</div>
</div>
<div class="lane">
<h3>Classic serving</h3>
<div class="flow">
<span class="node assumed">Request gateway</span><span class="arrow">→</span>
<span class="node assumed">Query router and cache</span><span class="arrow">→</span>
<span class="node assumed">Index shards and replicas</span><span class="arrow">→</span>
<span class="node assumed">Ranking and aggregation</span><span class="arrow">→</span>
<span class="node documented">Search results</span>
</div>
</div>
<div class="lane">
<h3>AI Mode serving</h3>
<div class="flow">
<span class="node documented">Query fan-out</span><span class="arrow">→</span>
<span class="node assumed">Parallel retrieval branches</span><span class="arrow">→</span>
<span class="node documented">Grounding</span><span class="arrow">→</span>
<span class="node documented">Response and links</span><span class="arrow">→</span>
<span class="node assumed">Classic fallback</span>
</div>
</div>
</div>
<div class="dependency-chain">
<span>Crawlable</span><span>→</span><span>Indexed</span><span>→</span><span>Retrieved</span><span>→</span><span>Ranked</span><span>→</span><span>Grounded and cited</span>
</div>
</section>
<section class="panel controls-panel">
<div class="section-heading">
<div>
<p class="eyebrow">Experiment</p>
<h2>Set the workload and failure model</h2>
</div>
</div>
<div class="controls">
<label>Scenario query
<input id="query" value="compare sleep tracking devices for travel">
</label>
<label>Mode
<select id="mode">
<option value="classic">Classic Search</option>
<option value="ai">AI Mode</option>
</select>
</label>
<label>Fan-out strategy
<select id="fanoutStrategy">
<option value="sequential">Sequential</option>
<option value="concurrent">Concurrent</option>
</select>
</label>
<label>Latency SLO in ms
<input id="slo" type="number" min="400" max="2000" step="50" value="700">
</label>
<label>Index shards
<input id="shardCount" type="number" min="2" max="32" value="8">
</label>
<label>Replicas per shard
<input id="replicaCount" type="number" min="1" max="5" value="3">
</label>
<label>Cache hit rate in percent
<input id="cacheRate" type="number" min="0" max="100" value="20">
</label>
<label>Replica failure rate in percent
<input id="failureRate" type="number" min="0" max="80" value="0">
</label>
<label>AI fan-out branches
<input id="fanoutCount" type="number" min="1" max="12" value="6">
</label>
<label>Fault scenario
<select id="faultScenario">
<option value="normal">Normal</option>
<option value="partial-outage">Two fan-out branches fail</option>
<option value="quorum-outage">Fan-out quorum lost</option>
<option value="replica-lag">Replica propagation lag</option>
</select>
</label>
</div>
<div class="actions">
<button id="runButton">Run simulation</button>
<button id="compareButton" class="secondary">Compare fan-out</button>
</div>
<p class="assumption">The 700 ms default is a lab SLO, not a published Google Search SLO. All component latencies are synthetic.</p>
</section>
<section class="metrics" aria-label="Simulation metrics">
<article><span>Outcome</span><strong id="metricOutcome">Not run</strong></article>
<article><span>Total latency</span><strong id="metricLatency">0 ms</strong></article>
<article><span>SLO</span><strong id="metricSlo">Not evaluated</strong></article>
<article><span>Cache</span><strong id="metricCache">No data</strong></article>
<article><span>AI sources</span><strong id="metricSources">0</strong></article>
<article><span>Freshness</span><strong id="metricFreshness">No scenario</strong></article>
</section>
<section class="panel">
<div class="section-heading">
<div>
<p class="eyebrow">Evidence</p>
<h2>Fan-out comparison</h2>
</div>
</div>
<p id="comparisonNote">Use the same seed to compare sequential and concurrent execution fairly.</p>
<div class="table-wrap">
<table>
<thead><tr><th>Strategy</th><th>Latency</th><th>Outcome</th><th>Sources</th><th>SLO</th></tr></thead>
<tbody id="comparisonBody"><tr><td colspan="5">Select Compare fan-out.</td></tr></tbody>
</table>
</div>
</section>
<section class="panel">
<div class="section-heading">
<div>
<p class="eyebrow">Critical path</p>
<h2>Stage timeline</h2>
</div>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Stage</th><th>Start</th><th>End</th><th>Duration</th><th>Status</th></tr></thead>
<tbody id="timelineBody"></tbody>
</table>
</div>
</section>
<section class="panel">
<div class="section-heading">
<div>
<p class="eyebrow">Design review</p>
<h2>System-design defense</h2>
</div>
</div>
<ul id="defenseList"></ul>
</section>
</main>
</body>
</html>
:root {
color-scheme: light;
--ink: #172033;
--muted: #5d6678;
--panel: #ffffff;
--line: #d9dfeb;
--blue: #2457d6;
--blue-soft: #eaf0ff;
--green: #087a55;
--green-soft: #e4f7ef;
--amber: #9a5b00;
--amber-soft: #fff3d8;
--red: #b42318;
--red-soft: #feeceb;
--background: #f4f7fb;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--background);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
}
.hero {
padding: 48px max(24px, calc((100vw - 1180px) / 2));
color: white;
background: linear-gradient(120deg, #14213d, #2457d6);
}
.hero h1 {
margin: 4px 0 8px;
font-size: clamp(2rem, 5vw, 3.5rem);
}
.hero p:last-child {
max-width: 760px;
margin-bottom: 0;
}
main {
width: min(1180px, calc(100% - 32px));
margin: 28px auto 64px;
}
.panel {
margin-bottom: 20px;
padding: 24px;
border: 1px solid var(--line);
border-radius: 18px;
background: var(--panel);
box-shadow: 0 10px 30px rgba(23, 32, 51, 0.06);
}
.section-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
margin-bottom: 18px;
}
.section-heading h2,
.lane h3 {
margin: 0;
}
.eyebrow {
margin: 0 0 4px;
color: var(--blue);
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.badge {
padding: 6px 10px;
border-radius: 999px;
color: var(--amber);
background: var(--amber-soft);
font-size: 0.8rem;
font-weight: 700;
}
.architecture {
display: grid;
gap: 14px;
}
.lane {
padding: 16px;
border: 1px solid var(--line);
border-radius: 14px;
background: #fbfcff;
}
.flow,
.dependency-chain {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
overflow-x: auto;
padding-bottom: 4px;
}
.node,
.dependency-chain span:nth-child(odd) {
min-width: max-content;
padding: 10px 12px;
border-radius: 10px;
font-size: 0.88rem;
font-weight: 700;
}
.node.documented {
color: var(--green);
background: var(--green-soft);
}
.node.assumed {
color: var(--blue);
background: var(--blue-soft);
}
.arrow {
color: var(--muted);
font-weight: 800;
}
.dependency-chain {
margin-top: 18px;
padding: 14px;
border-radius: 12px;
background: #172033;
color: white;
}
.dependency-chain span:nth-child(odd) {
background: rgba(255, 255, 255, 0.12);
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 14px;
}
label {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 0.85rem;
font-weight: 700;
}
input,
select,
button {
min-height: 42px;
border-radius: 9px;
font: inherit;
}
input,
select {
width: 100%;
border: 1px solid var(--line);
padding: 8px 10px;
color: var(--ink);
background: white;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
}
button {
border: 0;
padding: 0 18px;
color: white;
background: var(--blue);
cursor: pointer;
font-weight: 800;
}
button.secondary {
color: var(--blue);
background: var(--blue-soft);
}
.assumption,
#comparisonNote {
color: var(--muted);
font-size: 0.88rem;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.metrics article {
min-height: 110px;
padding: 16px;
border: 1px solid var(--line);
border-radius: 14px;
background: white;
}
.metrics span,
.metrics strong {
display: block;
}
.metrics span {
color: var(--muted);
font-size: 0.82rem;
font-weight: 700;
}
.metrics strong {
margin-top: 10px;
font-size: 1.08rem;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 11px 10px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
th {
color: var(--muted);
font-size: 0.78rem;
text-transform: uppercase;
}
.status-good {
color: var(--green);
font-weight: 800;
}
.status-warn {
color: var(--amber);
font-weight: 800;
}
.status-bad {
color: var(--red);
font-weight: 800;
}
#defenseList {
margin-bottom: 0;
padding-left: 22px;
}
#defenseList li {
margin: 8px 0;
}
@media (max-width: 700px) {
.hero {
padding-top: 34px;
padding-bottom: 34px;
}
.panel {
padding: 18px;
}
.section-heading {
display: grid;
}
}
"use strict";
const byId = (id) => document.getElementById(id);
let runCounter = 0;
function createRng(seed) {
let value = seed >>> 0;
return function nextRandom() {
value = (value * 1664525 + 1013904223) % 4294967296;
return value / 4294967296;
};
}
function randomInt(rng, min, max) {
return Math.floor(rng() * (max - min + 1)) + min;
}
function nextSeed() {
runCounter += 1;
return Math.floor(Math.random() * 1000000000) + runCounter;
}
function getConfig() {
return {
query: byId("query").value.trim() || "example query",
mode: byId("mode").value,
strategy: byId("fanoutStrategy").value,
slo: Number(byId("slo").value),
shards: Number(byId("shardCount").value),
replicas: Number(byId("replicaCount").value),
cacheRate: Number(byId("cacheRate").value),
failureRate: Number(byId("failureRate").value),
fanoutCount: Number(byId("fanoutCount").value),
faultScenario: byId("faultScenario").value
};
}
function simulateClassicQuery(config, rng) {
const timeline = [];
let cursor = 0;
function addStage(stage, duration, status) {
const start = cursor;
cursor += duration;
timeline.push({ stage, start, end: cursor, duration, status });
}
addStage("Request gateway", 10, "Accepted");
addStage("Query understanding", 24, "Intent parsed");
addStage("Retrieval", randomInt(rng, 70, 120), "Index candidates retrieved");
addStage("Ranking", 28, "Candidate scores merged");
addStage("Aggregation", 16, "Result page assembled");
addStage("Presentation", 12, "Visible to user");
return {
mode: "classic",
outcome: "Classic results",
latency: cursor,
sloMet: cursor <= config.slo,
cacheLabel: "Not modeled yet",
sources: 0,
freshness: "Not modeled yet",
timeline,
strategy: "Classic"
};
}
function buildDefense(result, config) {
return [
`Query: “${config.query}”. Simulation seed: ${result.seed}.`,
`SLO: the lab target is ${config.slo} ms. This run finished in ${result.latency} ms and ${result.sloMet ? "met" : "missed"} it.`,
"Scope boundary: public Google behavior is separated from synthetic component boundaries and synthetic latency assumptions."
];
}
function renderTimeline(timeline) {
byId("timelineBody").innerHTML = timeline.map((row) => `
<tr>
<td>${row.stage}</td>
<td>${row.start} ms</td>
<td>${row.end} ms</td>
<td>${row.duration} ms</td>
<td>${row.status}</td>
</tr>
`).join("");
}
function renderDefense(items) {
const list = byId("defenseList");
list.innerHTML = "";
items.forEach((item) => {
const entry = document.createElement("li");
entry.textContent = item;
list.appendChild(entry);
});
}
function renderResult(result, config) {
byId("metricOutcome").textContent = result.outcome;
byId("metricLatency").textContent = `${result.latency} ms`;
byId("metricSlo").textContent = result.sloMet ? `Met (${config.slo} ms)` : `Missed (${config.slo} ms)`;
byId("metricCache").textContent = result.cacheLabel;
byId("metricSources").textContent = String(result.sources);
byId("metricFreshness").textContent = result.freshness;
renderTimeline(result.timeline);
renderDefense(buildDefense(result, config));
}
function runCurrent() {
const config = getConfig();
const seed = nextSeed();
const result = simulateClassicQuery(config, createRng(seed));
result.seed = seed;
renderResult(result, config);
}
byId("runButton").addEventListener("click", runCurrent);
runCurrent();
Your classic Search simulator now exposes the full crawl-to-results foundation. Next, you will partition retrieval across shards and replicas so cache behavior, tail latency, failure, and freshness become measurable.
Scale Retrieval Across Shards and Replicas
Your Safari simulator currently compresses retrieval into one stage. That view hides the slow replicas and failed shards that shape a distributed query.
In this step, sharding divides retrieval work across the index. Replicas add routing choices while a cache exposes the latency benefit of repeated results. You will also model eventual consistency through a newly indexed page that has not reached every replica.
In this step, get ready to:
- Add controls for shards, replicas, caching, failures, and fault scenarios.
- Model parallel shard reads with fastest-healthy-replica selection.
- Measure cache latency and replica-propagation freshness.
Add retrieval controls
The simulator needs workload controls before it can expose retrieval tradeoffs. Each value changes one part of the illustrative index-serving model.
- Switch back to index.html in Visual Studio Code.
- Find the closing </label> for the Latency SLO in ms control.
- Add the four retrieval controls below that label by pasting this code:
<label>Index shards
<input id="shardCount" type="number" min="2" max="32" value="8">
</label>
<label>Replicas per shard
<input id="replicaCount" type="number" min="1" max="5" value="3">
</label>
<label>Cache hit rate in percent
<input id="cacheRate" type="number" min="0" max="100" value="20">
</label>
<label>Replica failure rate in percent
<input id="failureRate" type="number" min="0" max="80" value="0">
</label>
What Do These Controls Change?
- The shard count controls how many index partitions participate in a query.
- The replica count controls how many copies can serve each shard.
- The cache rate controls how often retrieval can skip shard work.
- The failure rate controls the chance that an individual replica is unavailable.
- Find the AI fan-out branches control in index.html.
- Add the fault selector below that label by pasting this code:
<label>Fault scenario
<select id="faultScenario">
<option value="normal">Normal</option>
<option value="partial-outage">Two fan-out branches fail</option>
<option value="quorum-outage">Fan-out quorum lost</option>
<option value="replica-lag">Replica propagation lag</option>
</select>
</label>
Why Use Named Fault Scenarios?
A named scenario makes a failure repeatable. The replica-lag option creates a specific freshness test while the outage options prepare the simulator for later AI failure policies.
- Save index.html.
- Refresh the simulator in Safari.
You should see controls for index shards, replicas per shard, cache hit rate, replica failure rate, and the fault scenario. The selected values should begin at 8 shards, 3 replicas, 20 percent cache hits, 0 percent failures, and Normal.
Do Not See the New Controls?
Confirm that every input sits inside its own label element. Check that the fault selector closes before the surrounding controls container.
Ask for targeted help with checking the retrieval-control markup.
Model shard and replica reads
A sharded read finishes when its required partitions respond. Selecting the fastest healthy replica reduces each shard's contribution to the critical path.
Why Fastest Healthy Replicas?
Replicas give the query router more than one serving candidate for each shard. The model chooses the quickest available copy to demonstrate tail-latency tolerance.
Asynchronous propagation creates a freshness cost. A fast copy can answer before it receives the newest indexed page.
- Switch back to app.js in Visual Studio Code.
- Find the closing brace of getConfig().
- Start simulateShardRead() below getConfig() by pasting this first connected part:
function simulateShardRead(config, rng) {
const selected = [];
let failedShards = 0;
let freshObserved = config.faultScenario !== "replica-lag";
for (let shard = 0; shard < config.shards; shard += 1) {
const healthyReplicas = [];
for (let replica = 0; replica < config.replicas; replica += 1) {
const healthy = rng() * 100 >= config.failureRate;
const workPerShard = Math.round(240 / config.shards);
let latency = 25 + workPerShard + randomInt(rng, 0, 70);
const hasFreshPage = config.faultScenario !== "replica-lag" || shard !== 0 || replica === 0;
if (config.faultScenario === "replica-lag" && shard === 0 && replica === 0) {
latency += 80;
}
if (healthy) {
healthyReplicas.push({ replica, latency, hasFreshPage });
}
}
How Are Replica Candidates Built?
- Each shard receives its own list of healthy replica candidates.
- The work-per-shard value decreases as the index is divided into more partitions.
- The failure rate independently determines whether each replica can answer.
- The fresh replica receives extra propagation latency during the replica-lag scenario.
- Complete simulateShardRead() by pasting this second connected part immediately below the first:
if (healthyReplicas.length === 0) {
failedShards += 1;
if (shard === 0 && config.faultScenario === "replica-lag") {
freshObserved = false;
}
continue;
}
healthyReplicas.sort((a, b) => a.latency - b.latency);
const chosen = healthyReplicas[0];
selected.push({ shard, ...chosen });
if (shard === 0 && config.faultScenario === "replica-lag") {
freshObserved = chosen.hasFreshPage;
}
}
const latency = selected.length > 0
? Math.max(...selected.map((item) => item.latency))
: config.slo;
return {
latency,
failedShards,
freshObserved,
selectedReplicas: selected
};
}
How Is the Shard Read Resolved?
- A shard is marked as failed when none of its replicas are healthy.
- Healthy replicas are sorted by latency before the first candidate is selected.
- The slowest selected shard determines the parallel retrieval latency.
- The first shard records whether its selected replica contains the newly indexed page.
The retrieval branch now needs to combine cache behavior with the shard-read result. Forced branch failures also need a deterministic rule for the AI scenarios used later.
- Add isForcedBranchFailure() below simulateShardRead() by pasting this code:
function isForcedBranchFailure(config, branchIndex) {
if (branchIndex < 0) {
return false;
}
if (config.faultScenario === "partial-outage") {
return branchIndex >= Math.max(0, config.fanoutCount - 2);
}
if (config.faultScenario === "quorum-outage") {
return branchIndex > 0;
}
return false;
}
What Does the Failure Rule Do?
A negative branch index identifies the classic retrieval path. Positive branch indexes let later AI simulations fail the same branches every time a named outage scenario runs.
- Start simulateRetrievalBranch() below isForcedBranchFailure() by pasting this first connected part:
function simulateRetrievalBranch(config, rng, branchIndex) {
if (isForcedBranchFailure(config, branchIndex)) {
return {
latency: randomInt(rng, 70, 110),
success: false,
cacheHit: false,
failedShards: config.shards,
freshObserved: false,
read: null
};
}
How Are Forced Failures Represented?
A forced failure returns a complete branch result without reading shards. The failed-shard count makes the branch loss visible to the later response policy.
- Complete simulateRetrievalBranch() by pasting this second connected part immediately below the first:
const cacheHit = rng() * 100 < config.cacheRate;
if (cacheHit) {
return {
latency: 30,
success: true,
cacheHit: true,
failedShards: 0,
freshObserved: config.faultScenario !== "replica-lag",
read: null
};
}
const read = simulateShardRead(config, rng);
return {
latency: 8 + 10 + read.latency + 28 + 16,
success: read.failedShards < config.shards,
cacheHit: false,
failedShards: read.failedShards,
freshObserved: read.freshObserved,
read
};
}
How Does a Retrieval Branch Work?
- A cache hit returns in a fixed synthetic latency without reading shards.
- A cache miss runs the shard model before adding routing, ranking, and aggregation work.
- A branch succeeds when at least one shard responds.
- The returned object carries latency, failures, cache state, and freshness into the query result.
- Add freshnessLabel() below simulateRetrievalBranch() by pasting this code:
function freshnessLabel(config, branch) {
if (config.faultScenario !== "replica-lag") {
return "All replicas modeled as current";
}
if (branch.cacheHit) {
return "Stale cache: new page absent";
}
return branch.freshObserved ? "Fresh replica selected" : "Stale replica selected";
}
What Does the Freshness Label Explain?
The label distinguishes normal reads from stale cache entries and replica selection. This turns a hidden consistency state into an observable metric.
- Select the existing simulateClassicQuery() function from its declaration through its closing brace.
- Replace the selected function with these two connected parts.
- Paste the first part:
function simulateClassicQuery(config, rng) {
const timeline = [];
let cursor = 0;
function addStage(stage, duration, status) {
const start = cursor;
cursor += duration;
timeline.push({ stage, start, end: cursor, duration, status });
}
addStage("Request gateway", 10, "Accepted");
addStage("Query understanding", 24, "Intent parsed");
const branch = simulateRetrievalBranch(config, rng, -1);
addStage("Result cache lookup", 8, branch.cacheHit ? "Hit" : "Miss");
if (branch.cacheHit) {
addStage("Serve cached result", 22, "Backend retrieval skipped");
} else {
addStage("Query routing", 10, `${config.shards} shards, ${config.replicas} replicas each`);
addStage("Parallel shard retrieval", branch.read.latency, `${branch.failedShards} failed shards`);
addStage("Ranking", 28, "Candidate scores merged");
addStage("Aggregation", 16, "Result page assembled");
}
addStage("Presentation", 12, "Visible to user");
How Does the Timeline Change?
The classic path records the cache lookup before choosing its retrieval route. Cache misses expose query routing and parallel shard retrieval in the timeline.
- Paste the second part immediately below the first:
let outcome = "Classic results";
if (!branch.success) {
outcome = "Classic retrieval failed";
} else if (branch.failedShards > 0) {
outcome = "Partial classic results";
}
return {
mode: "classic",
outcome,
latency: cursor,
sloMet: cursor <= config.slo,
cacheLabel: branch.cacheHit ? "1 result-cache hit" : "Result-cache miss",
sources: 0,
freshness: freshnessLabel(config, branch),
timeline,
failedShards: branch.failedShards,
strategy: "Classic"
};
}
How Is the Classic Outcome Chosen?
- A complete shard loss produces a failed classic retrieval.
- A smaller shard loss produces partial classic results.
- The returned cache and freshness labels feed their matching metric cards.
- The timeline total remains the value tested against the learner-defined SLO.
- Save app.js.
- Refresh the simulator in Safari.
- Select Classic Search in the Mode control.
- Select Normal in the Fault scenario control.
- Select Run simulation.
You should see a result-cache lookup in the timeline. A cache miss should also show query routing and parallel shard retrieval with a failed-shard count.
The Simulation Does Not Run?
Check that both connected parts of each function sit together inside app.js. Confirm that simulateClassicQuery() still ends before buildDefense() begins.
Ask for targeted help with debugging the shard simulation.
Measure cache and freshness tradeoffs
Cache hits remove shard work from the critical path. Replica lag reveals the freshness risk that appears when asynchronous copies serve reads before propagation completes.
- Set Mode to Classic Search.
- Set Cache hit rate in percent to 0.
- Set Replica failure rate in percent to 0.
- Set Fault scenario to Normal.
- Select Run simulation.
The cache metric should report Result-cache miss. Record the displayed total latency as your cache-miss latency.
- Set Cache hit rate in percent to 100.
- Select Run simulation.
The cache metric should report 1 result-cache hit. Record the displayed total latency as your cache-hit latency.
What Did the Cache Change?
The cache-hit run should be faster than the cache-miss run. Its timeline serves the cached result without query routing or shard retrieval.
That speed has a freshness boundary. A cached result can omit a page that was indexed after the cached response was created.
- Set Fault scenario to Replica propagation lag.
- Select Run simulation.
The freshness metric should report Stale cache: new page absent. This confirms that a fast cache hit can return an older view of the index.
Before the final run, which replica do you think the fastest-healthy-replica policy selects when the fresh copy carries extra propagation latency?
- Set Cache hit rate in percent to 0.
- Set Replicas per shard to 3.
- Set Replica failure rate in percent to 0.
- Select Run simulation.
The freshness metric should report Stale replica selected. The timeline should show parallel shard retrieval instead of the cached-result stage.
What Does the Stale Read Prove?
The fresh copy of shard zero is deliberately slower in this scenario. The routing policy selects a faster replica that has not received the newly indexed page.
This is the measurable cost of reading before asynchronous propagation finishes. Lower read latency can expose older data.
That is the retrieval layer working: your simulator now makes cache speed, shard failures, replica choice, and freshness visible in the same classic-query path.
Your classic path now exposes the tradeoffs hidden inside distributed retrieval. Next, you will send AI subqueries through that layer sequentially and watch their latency consume the SLO.
Expose Sequential AI Fan-Out
Your Safari simulator already exposes the latency costs of sharded classic retrieval. The next query path introduces more work through AI Mode.
Sequential query fan-out places every retrieval branch behind the previous branch. You will measure whether that path leaves enough of the latency SLO for grounding plus synthesis.
In this step, get ready to:
- Add controls for AI fan-out strategy and branch count.
- Implement the sequential AI query timeline.
- Measure the naive fan-out path against a 700 ms SLO.
Add the AI workload controls
The existing mode control chooses the query path. Two more controls define how retrieval branches complete and how many branches the planner creates.
- Switch back to index.html in Visual Studio Code.
- Find the Mode label inside the controls grid.
- Add the strategy selector immediately after the mode control by pasting this code:
<label>Fan-out strategy
<select id="fanoutStrategy">
<option value="sequential">Sequential</option>
<option value="concurrent">Concurrent</option>
</select>
</label>
What Does This Control Do?
- The fanoutStrategy identifier gives JavaScript access to the selected execution model.
- The sequential value makes each branch completion depend on the accumulated time of earlier branches.
- Save index.html.
- Return to Safari.
- Refresh the simulator.
You should see a Fan-out strategy selector beneath the mode control. Its initial value is Sequential.
Strategy Control Missing?
Confirm that the new label sits inside the existing controls container. Check that the opening and closing select tags are both present.
Ask for targeted help with finding the missing fan-out selector.
Fan-out breadth controls how many related retrieval branches compete for the same deadline. A larger value makes sequential accumulation easier to inspect.
- Switch back to index.html.
- Find the Replica failure rate in percent label.
- Add the branch-count control immediately after that label by pasting this code:
<label>AI fan-out branches
<input id="fanoutCount" type="number" min="1" max="12" value="6">
</label>
What Does the Branch Count Control?
- The fanoutCount value determines how many related retrieval branches the AI planner creates.
- The default value of 6 provides enough branches to expose sequential deadline pressure.
- Save index.html.
- Return to Safari.
- Refresh the simulator.
You should see an AI fan-out branches input with 6 as its default value.
Branch Count Missing?
Check that the input uses fanoutCount as its identifier. Confirm that the label remains inside the controls grid.
Ask for help with repairing the branch-count control.
The action area also needs a comparison control for the next experiment. Adding it now keeps both AI actions together.
- Switch back to index.html.
- Find the existing actions container.
- Replace that container with this completed action group:
<div class="actions">
<button id="runButton">Run simulation</button>
<button id="compareButton" class="secondary">Compare fan-out</button>
</div>
What Does This Action Group Provide?
- The runButton continues to run the selected mode.
- The compareButton creates a separate control for the strategy comparison added in the next step.
- The secondary class gives the comparison action its existing secondary-button style.
- Save index.html.
- Return to Safari.
- Refresh the simulator.
You should see Run simulation beside a secondary Compare fan-out button.
Buttons Not Aligned?
Confirm that both buttons are children of the same actions container. Check that the comparison button includes the secondary class.
Ask for help with fixing the action-button layout.
Implement the AI query timeline
The AI path begins with gateway handling plus query understanding. Its planner then creates several retrieval branches that share one end-to-end deadline.
The simulator reserves time for grounding before any branch starts. It also reserves time for response synthesis plus presentation.
- Switch back to app.js.
- Find the existing getConfig() function.
- Replace that function with this version:
function getConfig() {
return {
query: byId("query").value.trim() || "example query",
mode: byId("mode").value,
strategy: byId("fanoutStrategy").value,
slo: Number(byId("slo").value),
shards: Number(byId("shardCount").value),
replicas: Number(byId("replicaCount").value),
cacheRate: Number(byId("cacheRate").value),
failureRate: Number(byId("failureRate").value),
fanoutCount: Number(byId("fanoutCount").value),
faultScenario: byId("faultScenario").value
};
}
What Does This Configuration Collect?
- The strategy property records the selected fan-out execution model.
- The fanoutCount property converts the branch-count input into a number.
- The remaining properties preserve the classic retrieval controls used by every AI branch.
- Save app.js.
- Return to Safari.
- Refresh the simulator.
The existing classic simulation should still run when the page loads. This confirms that the new configuration fields have valid syntax.
Classic Simulation Stopped Loading?
Check the commas after mode and failureRate. Confirm that both new element identifiers match the HTML exactly.
Ask for targeted help with repairing the configuration reader.
The AI simulation is one connected function. The following chunks belong together in the listed order.
- Find the end of simulateClassicQuery() in app.js.
- Start simulateAiQuery() immediately below it with this first chunk:
function simulateAiQuery(config, rng) {
const timeline = [];
let cursor = 0;
function addStage(stage, duration, status) {
const start = cursor;
cursor += duration;
timeline.push({ stage, start, end: cursor, duration, status });
}
addStage("Request gateway", 10, "Accepted");
addStage("Query understanding", 24, "Complex query detected");
addStage("AI query planner", 55, `${config.fanoutCount} related searches planned`);
const reservedAfterFanout = 65 + 140 + 12;
const fanoutBudget = Math.max(60, config.slo - cursor - reservedAfterFanout);
const branches = [];
for (let index = 0; index < config.fanoutCount; index += 1) {
branches.push(simulateRetrievalBranch(config, rng, index));
}
How Does the Planner Protect the Deadline?
- The timeline cursor records when each AI stage starts and finishes.
- The reservedAfterFanout value protects 217 ms for grounding plus synthesis plus presentation.
- The fanoutBudget value gives retrieval branches the remaining portion of the SLO.
- Each branch reuses the sharded retrieval simulation from the previous step.
- Continue simulateAiQuery() immediately after the branch-creation loop with this chunk:
if (config.strategy === "sequential") {
let completion = 0;
branches.forEach((branch) => {
completion += branch.latency;
branch.completion = completion;
});
} else {
branches.forEach((branch) => {
branch.completion = branch.latency;
});
}
const rawFanoutWall = config.strategy === "sequential"
? branches.reduce((sum, branch) => sum + branch.latency, 0)
: Math.max(...branches.map((branch) => branch.latency));
const fanoutWall = Math.min(rawFanoutWall, fanoutBudget);
const usableBranches = branches.filter(
(branch) => branch.success && branch.completion <= fanoutBudget
);
const timedOut = branches.filter((branch) => branch.completion > fanoutBudget).length;
const failed = branches.filter((branch) => !branch.success).length;
addStage(
`${config.strategy === "sequential" ? "Sequential" : "Concurrent"} query fan-out`,
fanoutWall,
`${usableBranches.length} usable, ${failed} failed, ${timedOut} timed out, ${fanoutBudget} ms budget`
);
How Is Sequential Wall Time Measured?
- Sequential completion adds each branch latency to the accumulated completion time.
- The rawFanoutWall value records the full branch total before the deadline applies.
- The fanoutWall value stops the timeline at the retrieval deadline.
- The usable-branch filter excludes failures plus branches that finish after the budget.
- Continue the same function with its response-policy chunk:
const classicShadow = simulateRetrievalBranch(config, rng, -1);
const classicReadyAt = 10 + 24 + classicShadow.latency + 12;
let outcome;
let freshness;
if (usableBranches.length === config.fanoutCount) {
addStage("Grounding", 65, "All planned retrieval branches available");
addStage("Response synthesis", 140, "Grounded answer assembled");
addStage("Presentation", 12, "AI response and links visible");
outcome = "Full AI response";
freshness = usableBranches.some((branch) => branch.freshObserved)
? "Fresh page visible to at least one branch"
: freshnessLabel(config, usableBranches[0]);
} else if (usableBranches.length >= Math.min(2, config.fanoutCount)) {
addStage("Grounding", 65, "Missing branches excluded");
addStage("Response synthesis", 140, "Partial grounded answer assembled");
addStage("Presentation", 12, "Partial AI response and links visible");
outcome = "Partial AI response";
freshness = usableBranches.some((branch) => branch.freshObserved)
? "Fresh page visible to at least one branch"
: freshnessLabel(config, usableBranches[0]);
} else {
How Does the Simulation Judge Evidence?
- A full response requires every planned branch to finish successfully within the budget.
- A partial response requires at least two usable branches.
- The classicShadow branch measures when a classic result path could be ready.
- Freshness reports whether any usable branch observed the newly indexed page.
- Finish simulateAiQuery() with this final chunk:
const fallbackReadyAt = Math.max(cursor, classicReadyAt);
const fallbackDuration = Math.max(0, Math.min(config.slo, fallbackReadyAt) - cursor);
addStage("Classic-results fallback", fallbackDuration, "AI evidence threshold not met");
outcome = classicReadyAt <= config.slo
? "Classic-results fallback"
: "Deadline expired before fallback";
freshness = freshnessLabel(config, classicShadow);
}
const cacheHits = branches.filter((branch) => branch.cacheHit).length;
return {
mode: "ai",
outcome,
latency: cursor,
sloMet: cursor <= config.slo,
cacheLabel: `${cacheHits}/${config.fanoutCount} fan-out cache hits`,
sources: usableBranches.length,
freshness,
timeline,
failedShards: branches.reduce((sum, branch) => sum + branch.failedShards, 0),
strategy: config.strategy,
fanoutBudget,
rawFanoutWall
};
}
What Does the Function Return?
- The result uses the same metric shape as the classic simulation.
- The sources value reports how many branches survived the deadline.
- The fanoutBudget value exposes the allowed retrieval window.
- The rawFanoutWall value preserves the measured branch wall time before deadline enforcement.
- Save app.js.
- Return to Safari.
- Refresh the simulator.
The classic simulation should still populate the metrics plus timeline. This confirms that the complete AI function parses without breaking the existing path.
Simulator Stopped After the AI Function?
Confirm that the four chunks appear consecutively inside one simulateAiQuery() function. Check the braces around the full-response plus partial-response conditions.
Ask for help with finding the AI function syntax problem.
Run the sequential design
The run handler currently selects the classic simulation directly. It must choose the simulator that matches the mode control.
- Find the result assignment inside runCurrent().
- Replace that assignment with this mode-aware version:
const result = config.mode === "classic"
? simulateClassicQuery(config, rng)
: simulateAiQuery(config, rng);
How Does the Mode Switch Work?
The conditional keeps the existing classic path for the classic value. Every other mode value runs simulateAiQuery() with the same configuration plus random-number generator.
- Save app.js.
- Return to Safari.
- Refresh the simulator.
- Select AI Mode in the Mode control.
- Select Sequential in the Fan-out strategy control.
- Enter 6 in AI fan-out branches.
- Enter 0 in Cache hit rate in percent.
- Enter 0 in Replica failure rate in percent.
- Select Normal in the Fault scenario control.
- Enter 700 in Latency SLO in ms.
Before you run it, do you expect six sequential cache-miss branches to leave enough budget for a complete grounded response?
- Select Run simulation.
You should see a Sequential query fan-out timeline row with a 394 ms branch budget. Its status should report fewer than six usable branches.
The total reaches the 700 ms SLO boundary without producing a Full AI response. This shortfall is the intended result of the naive sequential design.
Why Did Sequential Fan-Out Fall Short?
Each sequential branch starts after the previous branch completes. Their latencies accumulate until later branches cross the retrieval deadline.
The simulator protects the post-retrieval budget instead of waiting indefinitely. That policy makes the missing evidence visible in the outcome plus source count.
AI Timeline Not Appearing?
Confirm that AI Mode remains selected before you run the simulation. Check that runCurrent() calls simulateAiQuery() for the AI value.
If all six branches appear usable, confirm that the cache rate is 0 plus the strategy is Sequential.
Ask for help with diagnosing the missing sequential shortfall.
You have measured the naive AI path at its failure point. Next, you will bound fan-out with concurrency plus turn missing evidence into a deliberate partial-result or fallback policy.
Meet the SLO with Concurrency and Fallback
Your sequential AI path now makes the bottleneck visible. Six cache-miss branches consume the latency budget before a complete grounded response can finish.
This step adds concurrent query fan-out, deadline propagation, graceful degradation, and classic fallback. A shared simulation seed keeps the performance comparison fair.
In this step, get ready to:
- Run AI retrieval branches concurrently within the remaining latency budget.
- Return partial AI output or classic results when branches fail.
- Compare both strategies with one seed and generate a system-design defense.
Run fan-out concurrently
Sequential branches add every retrieval duration to the critical path. Concurrent branches overlap that work, so the slowest completed branch determines the fan-out wall time.
Why propagate one deadline?
A request starts with one end-to-end budget. Each downstream stage receives the decreasing time that remains.
This prevents retrieval branches from consuming time already reserved for grounding, synthesis, and presentation.
- In app.js, find the branch completion logic inside simulateAiQuery() after the branches loop.
- Replace that logic through fanoutWall with this strategy-aware block:
if (config.strategy === "sequential") {
let completion = 0;
branches.forEach((branch) => {
completion += branch.latency;
branch.completion = completion;
});
} else {
branches.forEach((branch) => {
branch.completion = branch.latency;
});
}
const rawFanoutWall = config.strategy === "sequential"
? branches.reduce((sum, branch) => sum + branch.latency, 0)
: Math.max(...branches.map((branch) => branch.latency));
const fanoutWall = Math.min(rawFanoutWall, fanoutBudget);
How Does Concurrent Timing Work?
- Sequential completion accumulates each branch latency in completion.
- Concurrent completion records each branch latency from the same starting point.
- The concurrent wall time uses Math.max() because the slowest branch controls when fan-out finishes.
- The deadline caps fanoutWall at the available fanoutBudget.
- Save app.js.
- Return to Safari.
- Select AI Mode in the Mode control.
- Select Concurrent in the Fan-out strategy control.
- Select Normal in the Fault scenario control.
- Enter 0 in the Cache hit rate in percent control.
- Enter 6 in the AI fan-out branches control.
- Enter 700 in the Latency SLO in ms control.
Before you run the simulation, do you expect the fan-out wall time to follow the sum of all branches or the slowest branch?
- Select Run simulation.
The timeline now labels the stage as concurrent query fan-out. Its duration follows the slowest branch that finishes within the deadline.
Concurrent Timing Still Looks Sequential?
Confirm that the Fan-out strategy control shows Concurrent. Check that the concurrent branch assigns branch.latency directly to branch.completion.
Ask for help with checking the concurrent wall-time calculation.
Degrade with partial output and fallback
Concurrency reduces wall time, but it cannot guarantee that every source survives. The response policy now distinguishes enough evidence for a partial grounded answer from a lost quorum that requires classic-results fallback.
- In app.js, find simulateRetrievalBranch().
- Add this failure-policy helper immediately above that function:
function isForcedBranchFailure(config, branchIndex) {
if (branchIndex < 0) {
return false;
}
if (config.faultScenario === "partial-outage") {
return branchIndex >= Math.max(0, config.fanoutCount - 2);
}
if (config.faultScenario === "quorum-outage") {
return branchIndex > 0;
}
return false;
}
What Does the Failure Helper Do?
- A negative branch index identifies the classic shadow path, so forced AI branch failures leave it available.
- The partial-outage scenario fails the final two fan-out branches.
- The quorum-outage scenario leaves only the first AI branch available.
- Insert this guard at the start of simulateRetrievalBranch() before the cache lookup:
if (isForcedBranchFailure(config, branchIndex)) {
return {
latency: randomInt(rng, 70, 110),
success: false,
cacheHit: false,
failedShards: config.shards,
freshObserved: false,
read: null
};
}
How Does the Guard Model Failure?
A forced failure returns a deterministic branch result through the seeded random-number generator. The branch records failure without attempting cache or shard retrieval.
The response policy can now count failed branches using the same simulation inputs on every comparison run.
- Save app.js.
- Select Two fan-out branches fail in the Fault scenario control.
Before you run this scenario, how many of the six branches do you expect to remain usable?
- Select Run simulation.
The concurrent fan-out timeline reports four usable branches and two failed branches. This proves that the deterministic fault reaches the response path.
- In simulateAiQuery(), find the outcome logic immediately after classicReadyAt.
- Replace the outcome logic with this bounded response policy:
let outcome;
let freshness;
if (usableBranches.length === config.fanoutCount) {
addStage("Grounding", 65, "All planned retrieval branches available");
addStage("Response synthesis", 140, "Grounded answer assembled");
addStage("Presentation", 12, "AI response and links visible");
outcome = "Full AI response";
freshness = usableBranches.some((branch) => branch.freshObserved)
? "Fresh page visible to at least one branch"
: freshnessLabel(config, usableBranches[0]);
} else if (usableBranches.length >= Math.min(2, config.fanoutCount)) {
addStage("Grounding", 65, "Missing branches excluded");
addStage("Response synthesis", 140, "Partial grounded answer assembled");
addStage("Presentation", 12, "Partial AI response and links visible");
outcome = "Partial AI response";
freshness = usableBranches.some((branch) => branch.freshObserved)
? "Fresh page visible to at least one branch"
: freshnessLabel(config, usableBranches[0]);
} else {
const fallbackReadyAt = Math.max(cursor, classicReadyAt);
const fallbackDuration = Math.max(0, Math.min(config.slo, fallbackReadyAt) - cursor);
addStage("Classic-results fallback", fallbackDuration, "AI evidence threshold not met");
outcome = classicReadyAt <= config.slo
? "Classic-results fallback"
: "Deadline expired before fallback";
freshness = freshnessLabel(config, classicShadow);
}
How Does the Response Policy Degrade?
- All planned branches produce a full grounded AI response.
- At least two usable branches produce a partial grounded AI response with missing branches excluded.
- Fewer than two usable branches select the classic shadow result when it is ready before the end-to-end deadline.
- The fallback duration never extends beyond the configured SLO.
- Save app.js.
- Keep Two fan-out branches fail selected.
- Select Run simulation.
The Outcome metric now reports a partial AI response. The timeline shows grounding and synthesis continuing with four usable sources.
Before the next run, do you expect one surviving AI branch to meet the evidence threshold?
- Select Fan-out quorum lost in the Fault scenario control.
- Select Run simulation.
The Outcome metric now reports classic-results fallback when the shadow path meets the deadline. The timeline names the missing AI evidence threshold as the reason.
Failure Scenarios Produce the Same Outcome?
Confirm that the fault values passed to isForcedBranchFailure() remain partial-outage and quorum-outage. Check that the partial threshold uses at least two usable branches.
Ask for help with diagnosing the partial-output and fallback policy.
Compare strategies and defend the design
A useful architecture defense connects decisions to measured outcomes. The simulator now summarizes latency, sharding, replication, caching, failure policy, and freshness for the exact run on screen.
- In app.js, find buildDefense().
- Replace that function with this final evidence summary:
function buildDefense(result, config) {
const fanoutStatement = result.mode === "ai"
? `${result.strategy} fan-out used ${result.rawFanoutWall} ms before the ${result.fanoutBudget} ms branch deadline was enforced.`
: "Classic retrieval performs shard reads in parallel and waits for the slowest selected shard replica.";
return [
`Query: “${config.query}”. Simulation seed: ${result.seed}.`,
`SLO: the lab target is ${config.slo} ms. This run finished in ${result.latency} ms and ${result.sloMet ? "met" : "missed"} it.`,
`Fan-out: ${fanoutStatement}`,
`Sharding: ${config.shards} shards divide retrieval work, but the slowest required shard remains on the critical path.`,
`Replication: ${config.replicas} replicas per shard improve routing choices and failure tolerance at the cost of extra storage and propagation work.`,
`Caching: ${result.cacheLabel}. Hits reduce retrieval work, while misses expose shard and ranking latency.`,
`Failure policy: ${result.outcome}. The design prefers bounded partial output or a classic fallback over unbounded waiting.`,
`Freshness: ${result.freshness}. Asynchronous propagation can trade immediate consistency for lower write coordination.`,
"Scope boundary: crawling, indexing, serving, grounding, links, and query fan-out are based on public Google documentation. Shard counts, replica routing, component boundaries, synthetic latencies, and the lab SLO are design assumptions."
];
}
What Does the Defense Capture?
Each bullet ties a design decision to the current controls and measured result. The seed makes the evidence reproducible within the comparison.
The scope boundary keeps documented Google behavior separate from the simulator's illustrative architecture choices.
- Save app.js.
- Select Run simulation.
The System-design defense now lists the query, seed, SLO result, fan-out policy, sharding tradeoff, replication tradeoff, cache behavior, failure policy, freshness result, and scope boundary.
- Add this comparison function immediately below runCurrent() in app.js.
function compareStrategies() {
const baseConfig = { ...getConfig(), mode: "ai" };
const seed = nextSeed();
const sequentialConfig = { ...baseConfig, strategy: "sequential" };
const concurrentConfig = { ...baseConfig, strategy: "concurrent" };
const sequential = simulateAiQuery(sequentialConfig, createRng(seed));
const concurrent = simulateAiQuery(concurrentConfig, createRng(seed));
sequential.seed = seed;
concurrent.seed = seed;
byId("comparisonNote").textContent = `Both strategies used seed ${seed} and identical controls.`;
byId("comparisonBody").innerHTML = [sequential, concurrent].map((result) => `
<tr>
<td>${result.strategy}</td>
<td>${result.latency} ms</td>
<td>${result.outcome}</td>
<td>${result.sources}</td>
<td class="${result.sloMet ? "status-good" : "status-bad"}">${result.sloMet ? "Met" : "Missed"}</td>
</tr>
`).join("");
renderResult(concurrent, concurrentConfig);
}
How Does the Comparison Stay Fair?
- Both strategies receive identical controls through baseConfig.
- Both simulations create their random-number generator from the same seed.
- The comparison table displays latency, outcome, usable sources, and SLO status for each strategy.
- The main metrics render the concurrent result for closer inspection.
- Find the event-listener lines at the bottom of app.js.
- Replace the footer with these final three lines:
byId("runButton").addEventListener("click", runCurrent);
byId("compareButton").addEventListener("click", compareStrategies);
runCurrent();
What Does the Final Wiring Do?
The first listener runs one configured simulation. The second listener runs the shared-seed strategy comparison.
The final call populates the simulator as soon as Safari loads the page.
Use the tabs below to confirm that your final app.js matches the complete simulator.
✔️ Awesome, I've got everything
Great work. Save app.js before running the final scenarios.
ⓧ I'd like to double check the full code
"use strict";
const byId = (id) => document.getElementById(id);
let runCounter = 0;
function createRng(seed) {
let value = seed >>> 0;
return function nextRandom() {
value = (value * 1664525 + 1013904223) % 4294967296;
return value / 4294967296;
};
}
function randomInt(rng, min, max) {
return Math.floor(rng() * (max - min + 1)) + min;
}
function nextSeed() {
runCounter += 1;
return Math.floor(Math.random() * 1000000000) + runCounter;
}
function getConfig() {
return {
query: byId("query").value.trim() || "example query",
mode: byId("mode").value,
strategy: byId("fanoutStrategy").value,
slo: Number(byId("slo").value),
shards: Number(byId("shardCount").value),
replicas: Number(byId("replicaCount").value),
cacheRate: Number(byId("cacheRate").value),
failureRate: Number(byId("failureRate").value),
fanoutCount: Number(byId("fanoutCount").value),
faultScenario: byId("faultScenario").value
};
}
function simulateShardRead(config, rng) {
const selected = [];
let failedShards = 0;
let freshObserved = config.faultScenario !== "replica-lag";
for (let shard = 0; shard < config.shards; shard += 1) {
const healthyReplicas = [];
for (let replica = 0; replica < config.replicas; replica += 1) {
const healthy = rng() * 100 >= config.failureRate;
const workPerShard = Math.round(240 / config.shards);
let latency = 25 + workPerShard + randomInt(rng, 0, 70);
const hasFreshPage = config.faultScenario !== "replica-lag" || shard !== 0 || replica === 0;
if (config.faultScenario === "replica-lag" && shard === 0 && replica === 0) {
latency += 80;
}
if (healthy) {
healthyReplicas.push({ replica, latency, hasFreshPage });
}
}
if (healthyReplicas.length === 0) {
failedShards += 1;
if (shard === 0 && config.faultScenario === "replica-lag") {
freshObserved = false;
}
continue;
}
healthyReplicas.sort((a, b) => a.latency - b.latency);
const chosen = healthyReplicas[0];
selected.push({ shard, ...chosen });
if (shard === 0 && config.faultScenario === "replica-lag") {
freshObserved = chosen.hasFreshPage;
}
}
const latency = selected.length > 0
? Math.max(...selected.map((item) => item.latency))
: config.slo;
return {
latency,
failedShards,
freshObserved,
selectedReplicas: selected
};
}
function isForcedBranchFailure(config, branchIndex) {
if (branchIndex < 0) {
return false;
}
if (config.faultScenario === "partial-outage") {
return branchIndex >= Math.max(0, config.fanoutCount - 2);
}
if (config.faultScenario === "quorum-outage") {
return branchIndex > 0;
}
return false;
}
function simulateRetrievalBranch(config, rng, branchIndex) {
if (isForcedBranchFailure(config, branchIndex)) {
return {
latency: randomInt(rng, 70, 110),
success: false,
cacheHit: false,
failedShards: config.shards,
freshObserved: false,
read: null
};
}
const cacheHit = rng() * 100 < config.cacheRate;
if (cacheHit) {
return {
latency: 30,
success: true,
cacheHit: true,
failedShards: 0,
freshObserved: config.faultScenario !== "replica-lag",
read: null
};
}
const read = simulateShardRead(config, rng);
return {
latency: 8 + 10 + read.latency + 28 + 16,
success: read.failedShards < config.shards,
cacheHit: false,
failedShards: read.failedShards,
freshObserved: read.freshObserved,
read
};
}
function freshnessLabel(config, branch) {
if (config.faultScenario !== "replica-lag") {
return "All replicas modeled as current";
}
if (branch.cacheHit) {
return "Stale cache: new page absent";
}
return branch.freshObserved ? "Fresh replica selected" : "Stale replica selected";
}
function simulateClassicQuery(config, rng) {
const timeline = [];
let cursor = 0;
function addStage(stage, duration, status) {
const start = cursor;
cursor += duration;
timeline.push({ stage, start, end: cursor, duration, status });
}
addStage("Request gateway", 10, "Accepted");
addStage("Query understanding", 24, "Intent parsed");
const branch = simulateRetrievalBranch(config, rng, -1);
addStage("Result cache lookup", 8, branch.cacheHit ? "Hit" : "Miss");
if (branch.cacheHit) {
addStage("Serve cached result", 22, "Backend retrieval skipped");
} else {
addStage("Query routing", 10, `${config.shards} shards, ${config.replicas} replicas each`);
addStage("Parallel shard retrieval", branch.read.latency, `${branch.failedShards} failed shards`);
addStage("Ranking", 28, "Candidate scores merged");
addStage("Aggregation", 16, "Result page assembled");
}
addStage("Presentation", 12, "Visible to user");
let outcome = "Classic results";
if (!branch.success) {
outcome = "Classic retrieval failed";
} else if (branch.failedShards > 0) {
outcome = "Partial classic results";
}
return {
mode: "classic",
outcome,
latency: cursor,
sloMet: cursor <= config.slo,
cacheLabel: branch.cacheHit ? "1 result-cache hit" : "Result-cache miss",
sources: 0,
freshness: freshnessLabel(config, branch),
timeline,
failedShards: branch.failedShards,
strategy: "Classic"
};
}
function simulateAiQuery(config, rng) {
const timeline = [];
let cursor = 0;
function addStage(stage, duration, status) {
const start = cursor;
cursor += duration;
timeline.push({ stage, start, end: cursor, duration, status });
}
addStage("Request gateway", 10, "Accepted");
addStage("Query understanding", 24, "Complex query detected");
addStage("AI query planner", 55, `${config.fanoutCount} related searches planned`);
const reservedAfterFanout = 65 + 140 + 12;
const fanoutBudget = Math.max(60, config.slo - cursor - reservedAfterFanout);
const branches = [];
for (let index = 0; index < config.fanoutCount; index += 1) {
branches.push(simulateRetrievalBranch(config, rng, index));
}
if (config.strategy === "sequential") {
let completion = 0;
branches.forEach((branch) => {
completion += branch.latency;
branch.completion = completion;
});
} else {
branches.forEach((branch) => {
branch.completion = branch.latency;
});
}
const rawFanoutWall = config.strategy === "sequential"
? branches.reduce((sum, branch) => sum + branch.latency, 0)
: Math.max(...branches.map((branch) => branch.latency));
const fanoutWall = Math.min(rawFanoutWall, fanoutBudget);
const usableBranches = branches.filter(
(branch) => branch.success && branch.completion <= fanoutBudget
);
const timedOut = branches.filter((branch) => branch.completion > fanoutBudget).length;
const failed = branches.filter((branch) => !branch.success).length;
addStage(
`${config.strategy === "sequential" ? "Sequential" : "Concurrent"} query fan-out`,
fanoutWall,
`${usableBranches.length} usable, ${failed} failed, ${timedOut} timed out, ${fanoutBudget} ms budget`
);
const classicShadow = simulateRetrievalBranch(config, rng, -1);
const classicReadyAt = 10 + 24 + classicShadow.latency + 12;
let outcome;
let freshness;
if (usableBranches.length === config.fanoutCount) {
addStage("Grounding", 65, "All planned retrieval branches available");
addStage("Response synthesis", 140, "Grounded answer assembled");
addStage("Presentation", 12, "AI response and links visible");
outcome = "Full AI response";
freshness = usableBranches.some((branch) => branch.freshObserved)
? "Fresh page visible to at least one branch"
: freshnessLabel(config, usableBranches[0]);
} else if (usableBranches.length >= Math.min(2, config.fanoutCount)) {
addStage("Grounding", 65, "Missing branches excluded");
addStage("Response synthesis", 140, "Partial grounded answer assembled");
addStage("Presentation", 12, "Partial AI response and links visible");
outcome = "Partial AI response";
freshness = usableBranches.some((branch) => branch.freshObserved)
? "Fresh page visible to at least one branch"
: freshnessLabel(config, usableBranches[0]);
} else {
const fallbackReadyAt = Math.max(cursor, classicReadyAt);
const fallbackDuration = Math.max(0, Math.min(config.slo, fallbackReadyAt) - cursor);
addStage("Classic-results fallback", fallbackDuration, "AI evidence threshold not met");
outcome = classicReadyAt <= config.slo
? "Classic-results fallback"
: "Deadline expired before fallback";
freshness = freshnessLabel(config, classicShadow);
}
const cacheHits = branches.filter((branch) => branch.cacheHit).length;
return {
mode: "ai",
outcome,
latency: cursor,
sloMet: cursor <= config.slo,
cacheLabel: `${cacheHits}/${config.fanoutCount} fan-out cache hits`,
sources: usableBranches.length,
freshness,
timeline,
failedShards: branches.reduce((sum, branch) => sum + branch.failedShards, 0),
strategy: config.strategy,
fanoutBudget,
rawFanoutWall
};
}
function buildDefense(result, config) {
const fanoutStatement = result.mode === "ai"
? `${result.strategy} fan-out used ${result.rawFanoutWall} ms before the ${result.fanoutBudget} ms branch deadline was enforced.`
: "Classic retrieval performs shard reads in parallel and waits for the slowest selected shard replica.";
return [
`Query: “${config.query}”. Simulation seed: ${result.seed}.`,
`SLO: the lab target is ${config.slo} ms. This run finished in ${result.latency} ms and ${result.sloMet ? "met" : "missed"} it.`,
`Fan-out: ${fanoutStatement}`,
`Sharding: ${config.shards} shards divide retrieval work, but the slowest required shard remains on the critical path.`,
`Replication: ${config.replicas} replicas per shard improve routing choices and failure tolerance at the cost of extra storage and propagation work.`,
`Caching: ${result.cacheLabel}. Hits reduce retrieval work, while misses expose shard and ranking latency.`,
`Failure policy: ${result.outcome}. The design prefers bounded partial output or a classic fallback over unbounded waiting.`,
`Freshness: ${result.freshness}. Asynchronous propagation can trade immediate consistency for lower write coordination.`,
"Scope boundary: crawling, indexing, serving, grounding, links, and query fan-out are based on public Google documentation. Shard counts, replica routing, component boundaries, synthetic latencies, and the lab SLO are design assumptions."
];
}
function renderTimeline(timeline) {
byId("timelineBody").innerHTML = timeline.map((row) => `
<tr>
<td>${row.stage}</td>
<td>${row.start} ms</td>
<td>${row.end} ms</td>
<td>${row.duration} ms</td>
<td>${row.status}</td>
</tr>
`).join("");
}
function renderDefense(items) {
const list = byId("defenseList");
list.innerHTML = "";
items.forEach((item) => {
const entry = document.createElement("li");
entry.textContent = item;
list.appendChild(entry);
});
}
function renderResult(result, config) {
byId("metricOutcome").textContent = result.outcome;
byId("metricLatency").textContent = `${result.latency} ms`;
byId("metricSlo").textContent = result.sloMet
? `Met (${config.slo} ms)`
: `Missed (${config.slo} ms)`;
byId("metricSlo").className = result.sloMet ? "status-good" : "status-bad";
byId("metricCache").textContent = result.cacheLabel;
byId("metricSources").textContent = String(result.sources);
byId("metricFreshness").textContent = result.freshness;
renderTimeline(result.timeline);
renderDefense(buildDefense(result, config));
}
function runCurrent() {
const config = getConfig();
const seed = nextSeed();
const rng = createRng(seed);
const result = config.mode === "classic"
? simulateClassicQuery(config, rng)
: simulateAiQuery(config, rng);
result.seed = seed;
renderResult(result, config);
}
function compareStrategies() {
const baseConfig = { ...getConfig(), mode: "ai" };
const seed = nextSeed();
const sequentialConfig = { ...baseConfig, strategy: "sequential" };
const concurrentConfig = { ...baseConfig, strategy: "concurrent" };
const sequential = simulateAiQuery(sequentialConfig, createRng(seed));
const concurrent = simulateAiQuery(concurrentConfig, createRng(seed));
sequential.seed = seed;
concurrent.seed = seed;
byId("comparisonNote").textContent = `Both strategies used seed ${seed} and identical controls.`;
byId("comparisonBody").innerHTML = [sequential, concurrent].map((result) => `
<tr>
<td>${result.strategy}</td>
<td>${result.latency} ms</td>
<td>${result.outcome}</td>
<td>${result.sources}</td>
<td class="${result.sloMet ? "status-good" : "status-bad"}">${result.sloMet ? "Met" : "Missed"}</td>
</tr>
`).join("");
renderResult(concurrent, concurrentConfig);
}
byId("runButton").addEventListener("click", runCurrent);
byId("compareButton").addEventListener("click", compareStrategies);
runCurrent();
How Should You Use This Reference?
Compare this reference with your saved app.js. The identifiers and control values must match exactly for the deterministic scenarios to work.
- Save app.js.
- Return to Safari.
- Refresh the page.
- Select Normal in the Fault scenario control.
- Enter 0 in the Cache hit rate in percent control.
- Enter 6 in the AI fan-out branches control.
- Enter 700 in the Latency SLO in ms control.
Before you compare the strategies, which row do you expect to use the sum of branch durations?
- Select Compare fan-out.
The comparison note reports one shared seed. The table shows sequential wall time based on accumulated branch durations, while concurrent wall time follows the slowest completed branch.
Now predict which degradation policy keeps the system useful when two branches fail and when quorum disappears.
- Select Two fan-out branches fail in the Fault scenario control.
- Select Run simulation.
You should see a partial AI response with four usable sources. The design continues with bounded evidence instead of waiting for failed branches.
- Select Fan-out quorum lost in the Fault scenario control.
- Select Run simulation.
You should see classic-results fallback when the shadow path is ready within the SLO. That completes the bounded response policy for success, partial failure, and lost quorum.
The Comparison Table Does Not Update?
Confirm that compareButton registers compareStrategies as its click listener. Check Safari's developer console for a JavaScript syntax error if the page stops responding.
Ask for help with debugging the final comparison and fallback checks.
Strong finish. Your simulator now turns concurrency, deadlines, partial failure, and fallback into measurable system-design evidence.
Secret mission
Survive a Traffic Spike
Push the concurrent AI path through a synthetic traffic spike. Tune one control and defend the resulting latency, availability, freshness, or response-quality tradeoff with measured evidence.
Clean Up Your Resources
Clean Up Your Resources
Your Search Architecture Lab is a static local project. It creates no cloud resources or ongoing costs, so your choice only affects the files on your Mac.
Resources you used:
- The local search-architecture-lab folder.
- The index.html file.
- The styles.css file.
- The app.js file.
Keep everything running
No action needed. Choose this if you want to rerun the simulator or test more distributed-system tradeoffs.
- Keep the search-architecture-lab folder in its current location.
- Use the same local folder for future simulator experiments.
The simulator remains ready to open locally. Keeping it creates no ongoing charges.
Pause - I'll come back to this later
Close the browser tab or editor to clear your workspace. The static lab has no server process to stop.
- Close the Safari tab that displays the lab.
- Switch back to Visual Studio Code from earlier.
- Check that the project files show no unsaved indicators.
- Close Visual Studio Code.
Your files remain inside search-architecture-lab. You can reopen the simulator whenever you return.
Delete - I don't want to use this again
Remove all project files to return to a clean local workspace. This deletion is permanent unless you have a separate backup.
- Switch back to Visual Studio Code from earlier.
- Create a fresh integrated Terminal for the open search-architecture-lab folder.
- Check that the Terminal prompt ends with search-architecture-lab.
- Remove the project folder by running these commands:
cd ..
rm -rf search-architecture-lab
ls
What do these commands do?
- The cd command moves Terminal to the parent folder that contains the lab.
- The rm command removes the search-architecture-lab folder.
- The -rf flags remove the folder contents recursively without a confirmation prompt.
- The ls command prints the remaining items so you can verify the deletion.
You should see the parent folder's remaining items. The search-architecture-lab folder should be absent.
Still see the project folder?
- Confirm that the fresh Terminal started inside search-architecture-lab before you ran the commands.
- Check that the folder name uses the exact lowercase spelling.
Help me safely delete my local Search Architecture Lab folder.
Nice Work!
Nice Work!
You did it! Your Search Architecture Lab now turns distributed-search tradeoffs into measurable browser evidence.
You've learned how to:
- Build a dependency-free browser simulator for classic Google Search query paths plus AI Mode query paths. Trace each stage from request gateway through presentation. Measure every run against an explicit latency SLO.
- Scale retrieval with sharding. Select the fastest healthy replica for each shard. Compare cache hits with misses. Demonstrate eventual consistency through a stale read during propagation.
- Compare sequential execution with concurrent query fan-out. Bound each branch through a propagated deadline derived from the end-to-end SLO. Preserve usefulness through partial AI output. Trigger classic-results fallback after quorum loss. Generate an evidence-based system-design defense.
- Secret Mission: Stress-test the final simulator under a synthetic traffic spike. Identify the dominant measured bottleneck. Tune exactly one control. Defend the mitigation with one explicit tradeoff.
Ready to quiz yourself? This final check tests your architecture decisions.