Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions benchmarker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
42 changes: 42 additions & 0 deletions benchmarker/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: "058e0af2c2b57e369d905a03ac9748b0ebf543c6"
channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
- platform: android
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
- platform: ios
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
- platform: linux
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
- platform: macos
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
- platform: windows
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6

# User provided section

# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
135 changes: 135 additions & 0 deletions benchmarker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# http_cache_stream benchmarker

A standalone Flutter app that benchmarks HTTP responses served by
`http_cache_stream`'s local cache server, and compares them against requests
that bypass the package entirely.

It depends on the package by path (`http_cache_stream: {path: ../}`), so it
always benchmarks the working copy of the repository.

Supported platforms: Android, iOS, Linux, macOS, Windows. Web is unsupported —
the benchmark relies on `dart:io` and `dart:isolate`.

```bash
cd benchmarker
flutter run -d <device>
```

## What it measures

Requests are issued by a pool of **long-lived worker isolates**. Each isolate
builds exactly one `http.Client` when it starts and reuses it for every request,
so connection pools stay warm both within and between runs. The pool is only
respawned when the concurrency or the client implementation changes.

### Inputs

| Input | Meaning |
| --- | --- |
| Source URL | The remote URL under test. |
| Concurrency | Number of worker isolates. |
| Total requests | Requests issued in total, divided evenly between the workers (the remainder goes to the lowest-numbered workers). |
| Request range | Which bytes each request asks for: a mode plus a slider bounding the region. |

### Request range

| Mode | Behavior |
| --- | --- |
| **Full response** | No `Range` header; every request downloads the whole source. |
| **Fixed range** | Every request asks for the same slider selection. |
| **Sequential windows** | The selection is divided between the requests: request *n* asks for the *n*-th window, each the same size, one starting where the last ended. |

Sequential windows are sized by the total request count — `window = ⌈range ÷
requests⌉` — so the run walks the selected region exactly once. The final window
slides back to end on the last byte, which keeps every request the same size at
the cost of a small overlap when the division isn't even. Windows are assigned
by each request's global sequence number, so workers cover consecutive blocks of
the region concurrently.

Both partial modes need the source's size to express a selection in bytes, so
they stay disabled until **Fetch length** probes the URL — a `HEAD`, falling
back to a one-byte range request, which also reveals whether the server honors
`Range` at all. The probed length is dropped as soon as the URL is edited, so a
stale size can never be applied to a different source.

Each request then carries `Range: bytes=<start>-<end>`, and the response's byte
count is verified against its `Content-Length` exactly as a full response is. A
run whose range requests come back as anything other than `206 Partial Content`
logs a warning once — the server is likely ignoring the range.

### Run types

| Type | Behavior |
| --- | --- |
| **Pre-cached** | The file is fully downloaded first, then every benchmarked request is served from the completed cache file. |
| **Non-cached** | The cache files are deleted first, so the requests race the cache download. |
| **Direct** | `http_cache_stream` is bypassed; workers request the source URL. |

### HTTP client

The client each worker uses is chosen from a registry of
[`HttpClientBuilder`](lib/src/benchmark/http_client_builder.dart)s — top-level
functions sent to the isolate at spawn time. Add an entry to
`kHttpClientOptions` to benchmark another implementation (for example
`cupertino_http` or `cronet_http`); nothing else needs to change.

### Output

Per run, aggregated from the results streamed back by the workers and refreshed
roughly four times a second:

- Average, p50/p90/p99, min and max for **time to response headers**, **time to
first byte**, and **time to completion**.
- **Requests per second**, **bytes per second**, total bytes, average response
size, and elapsed wall-clock time.
- Outcome breakdown: verified, unverified (no `Content-Length`), byte
mismatches, HTTP errors, and failures.

Every response's received byte count is checked against its `Content-Length`; a
mismatch is reported as a problem rather than a success.

The copy button at the top right of the statistics panel puts the whole run on
the clipboard — source URL, target URL, cache type, request range, client and
concurrency alongside the numbers — as either an aligned plain-text summary or
JSON for feeding into other tooling.

For cache-server runs, the page also shows live download progress
(`x / y bytes`, percentage) taken from `HttpCacheStream.cacheStateStream`, and
errors emitted by that stream are written to the log panel along with the run's
status lines.

## Layout

```
lib/
main.dart app entry; initializes HttpCacheManager
src/benchmark/
benchmark_config.dart inputs, run types, request distribution
benchmark_controller.dart run orchestration and aggregation
benchmark_report.dart clipboard reports (text and JSON)
benchmark_stats.dart timing/percentile accumulation
benchmark_worker.dart worker isolate entry point
http_client_builder.dart selectable http client implementations
source_probe.dart content-length / range-support probe
worker_pool.dart long-lived isolate pool
worker_protocol.dart messages exchanged with the workers
src/ui/
benchmark_form.dart input state, owned by the page
benchmark_page.dart page layout
widgets/ config, progress, stats and log panels
```

## Tests

```bash
cd benchmarker
flutter test
```

The suite covers request distribution and statistics, drives real worker
isolates against a local origin server, and runs all three benchmark types
end-to-end through a real `HttpCacheManager`.

> When testing against a **loopback** origin, address it as `localhost` rather
> than `127.0.0.1`: `http_cache_stream` rejects source URLs whose host matches
> the cache server's own host.
28 changes: 28 additions & 0 deletions benchmarker/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
14 changes: 14 additions & 0 deletions benchmarker/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/

# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
45 changes: 45 additions & 0 deletions benchmarker/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}

android {
namespace = "com.httpcachestream.benchmarker"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.httpcachestream.benchmarker"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}

buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}

kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}

flutter {
source = "../.."
}
7 changes: 7 additions & 0 deletions benchmarker/android/app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
47 changes: 47 additions & 0 deletions benchmarker/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="benchmarker"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.httpcachestream.benchmarker

import io.flutter.embedding.android.FlutterActivity

class MainActivity : FlutterActivity()
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />

<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Loading
Loading