Showing preview only (1,037K chars total). Download the full file or copy to clipboard to get everything.
Repository: Zoxc/crusader
Branch: master
Commit: 5d1e6e76fb9b
Files: 64
Total size: 1002.4 KB
Directory structure:
gitextract_275plfem/
├── .dockerignore
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── release.md
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── android/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── app/
│ │ ├── build.gradle
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── zoxc/
│ │ │ └── crusader/
│ │ │ └── MainActivity.java
│ │ └── res/
│ │ └── values/
│ │ ├── colors.xml
│ │ └── themes.xml
│ ├── build.gradle
│ ├── debugInstall.ps1
│ ├── gradle/
│ │ └── wrapper/
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── settings.gradle
│ └── src/
│ └── lib.rs
├── data/
│ ├── v0.crr
│ ├── v1.crr
│ └── v2.crr
├── docker/
│ ├── README.md
│ ├── remote-static.Dockerfile
│ └── server-static.Dockerfile
├── docs/
│ ├── BUILDING.md
│ ├── CLI.md
│ ├── LOCAL_TESTS.md
│ ├── RESULTS.md
│ └── TROUBLESHOOTING.md
├── media/
│ ├── Crusader Screen Shots.md
│ └── batch_add_border.sh
└── src/
├── Cargo.toml
├── crusader/
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
├── crusader-gui/
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
├── crusader-gui-lib/
│ ├── Cargo.toml
│ └── src/
│ ├── client.rs
│ └── lib.rs
└── crusader-lib/
├── Cargo.toml
├── UFL.txt
├── assets/
│ ├── vue.js
│ └── vue.prod.js
├── build.rs
└── src/
├── common.rs
├── discovery.rs
├── file_format.rs
├── latency.rs
├── lib.rs
├── peer.rs
├── plot.rs
├── protocol.rs
├── remote.html
├── remote.rs
├── serve.rs
└── test.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
/src/target
================================================
FILE: .gitattributes
================================================
src/crusader-lib/assets/* linguist-vendored
================================================
FILE: .github/workflows/ci.yml
================================================
name: ci
on:
push:
branches: [master]
pull_request:
env:
CARGO_INCREMENTAL: 0
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build server-only binary
run: cargo build -p crusader --no-default-features
working-directory: src
- name: Build
run: cargo build
working-directory: src
- name: Lint
run: cargo clippy --all -- -D warnings
working-directory: src
- name: Format
run: cargo fmt --all -- --check
working-directory: src
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Install Rust targets
run: >
rustup target add
aarch64-linux-android
armv7-linux-androideabi
x86_64-linux-android
i686-linux-android
- name: Install cargo-ndk
run: cargo install cargo-ndk
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Build Android Rust crates
working-directory: android
run: cargo ndk -t arm64-v8a -o app/src/main/jniLibs/ -- build
- name: Build Android APK
working-directory: android
run: ./gradlew buildDebug
# Wait for a new cargo ndk release for better clippy support
#- name: Lint
# run: cargo ndk -t arm64-v8a -- clippy --all -- -D warnings
# working-directory: android
- name: Format
run: cargo fmt --all -- --check
working-directory: android
================================================
FILE: .github/workflows/release.md
================================================
Crusader has pre-built binaries for a number of operating systems. Download the appropriate binary below for your OS.
================================================
FILE: .github/workflows/release.yml
================================================
name: release
on:
push:
tags:
- "v*"
env:
CARGO_INCREMENTAL: 0
jobs:
create-release:
name: create-release
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.release.outputs.upload_url }}
permissions:
contents: write
steps:
- uses: actions/checkout@v2
- name: Get the release version from the tag
shell: bash
run: echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Create GitHub release
id: release
uses: actions/create-release@v1.1.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.TAG_NAME }}
release_name: Automated build of ${{ env.TAG_NAME }}
prerelease: true
body_path: .github/workflows/release.md
release-assets:
name: Release assets
needs: create-release
runs-on: ${{ matrix.build.os }}
strategy:
fail-fast: false
matrix:
build:
- os: ubuntu-latest
target: arm-unknown-linux-musleabihf
friendly: Linux-ARM-32-bit
exe_postfix:
cargo: cross
gui: false
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
friendly: Linux-ARM-64-bit
exe_postfix:
cargo: cross
gui: false
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
friendly: Linux-X86-64-bit
exe_postfix:
cargo: cargo
gui: false
- os: macos-latest
target: aarch64-apple-darwin
friendly: macOS-ARM-64-bit
exe_postfix:
cargo: cargo
gui: true
- os: macos-latest
target: x86_64-apple-darwin
friendly: macOS-X86-64-bit
exe_postfix:
cargo: cargo
gui: true
- os: windows-latest
target: i686-pc-windows-msvc
friendly: Windows-X86-32-bit
exe_postfix: .exe
cargo: cargo
gui: true
- os: windows-latest
target: x86_64-pc-windows-msvc
friendly: Windows-X86-64-bit
exe_postfix: .exe
cargo: cargo
gui: true
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
target: ${{ matrix.build.target }}
- name: Install cross
if: matrix.build.cargo == 'cross'
run: cargo install cross
- name: Install and use musl
if: matrix.build.os == 'ubuntu-latest' && matrix.build.cargo != 'cross'
run: |
sudo apt-get install -y --no-install-recommends musl-tools
echo "CC=musl-gcc" >> $GITHUB_ENV
echo "AR=ar" >> $GITHUB_ENV
- name: Build command line binary
if: ${{ !matrix.build.gui }}
run: ${{ matrix.build.cargo }} build -p crusader --target ${{ matrix.build.target }} --release
working-directory: src
env:
RUSTFLAGS: "-C target-feature=+crt-static"
- name: Build
if: matrix.build.gui
run: ${{ matrix.build.cargo }} build --target ${{ matrix.build.target }} --release
working-directory: src
env:
RUSTFLAGS: "-C target-feature=+crt-static"
- name: Build output
shell: bash
run: |
staging="Crusader-${{ matrix.build.friendly }}"
mkdir -p "$staging"
cp src/target/${{ matrix.build.target }}/release/crusader${{ matrix.build.exe_postfix }} "$staging/"
- name: Copy GUI binary
if: matrix.build.gui
shell: bash
run: |
cp src/target/${{ matrix.build.target }}/release/crusader-gui${{ matrix.build.exe_postfix }} "crusader-${{ matrix.build.friendly }}/"
- name: Archive output
if: matrix.build.os == 'windows-latest'
shell: bash
run: |
staging="Crusader-${{ matrix.build.friendly }}"
7z a "$staging.zip" "$staging"
echo "ASSET=$staging.zip" >> $GITHUB_ENV
- name: Archive output
if: matrix.build.os != 'windows-latest'
shell: bash
run: |
staging="Crusader-${{ matrix.build.friendly }}"
tar czf "$staging.tar.gz" "$staging"
echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV
- name: Upload archive
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_name: ${{ env.ASSET }}
asset_path: ${{ env.ASSET }}
asset_content_type: application/octet-stream
release-android-assets:
name: Android APK
needs: create-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Install Rust targets
run: >
rustup target add
aarch64-linux-android
armv7-linux-androideabi
x86_64-linux-android
i686-linux-android
- name: Install cargo-ndk
run: cargo install cargo-ndk
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Build Android Rust crates
working-directory: android
run: >
cargo ndk
-t arm64-v8a
-t armeabi-v7a
-t x86_64
-t x86
-o app/src/main/jniLibs/ -- build --release
- name: Decode Keystore
env:
ENCODED_STRING: ${{ secrets.KEYSTORE }}
run: echo "$ENCODED_STRING" | base64 -di > ../android.keystore
- name: Build Android APK
working-directory: android
run: ./gradlew build
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
- name: Upload APK
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_name: Crusader-Android.apk
asset_path: android/app/build/outputs/apk/release/app-release.apk
asset_content_type: application/octet-stream
================================================
FILE: .gitignore
================================================
/src/target
================================================
FILE: CHANGELOG.md
================================================
# CHANGELOG
The **Crusader Network Tester** measures network rates and latency
in the presence of upload and download traffic.
It produces plots of the traffic rates,
latency and packet loss.
This file lists the changes that have occurred since January 2024 in the project.
## Unreleased
## 0.3.2 - 2024-10-03
* Fix saved raw data path printed after a test
* Avoid duplicate legends when plotting transferred bytes
* Make `--plot-transferred` increase default plot height
* Fix unique output path generation
## 0.3.1 - 2024-09-30
* Increase samples used for clock synchronization and idle latency measurement
* Clock synchronization now uses the average of the lowest 1/3rd of samples
* Adjust for clock drift in tests
* Fix connecting to servers on non-standard port with peers
* Make discovery more robust by sending multiple packets
## 0.3 - 2024-09-16
* Show throughput, latency, and packet loss summaries in plots and with the `test` command
* Rename both option to bidirectional
* Rename `--latency-peer-server` to `--latency-peer-address`
* Continuous clock synchronization with the latency monitor
* Support opening result files in the GUI by drag and drop
* Add `--out-name` command line option to specify result filename prefix
* Change filename prefix for both raw result and plots to `test`
* Add file dialog to save options in GUI
* Add buttons to save and load from the `crusader-results` folder in GUI
* Add an `export` command line command to convert result files to JSON
* Change timeout when connecting a peer to the server to 8 seconds
* Hide advanced parameters in GUI
* Add a reset parameters button in GUI
* Add an option to measure latency-only for the client in the GUI
* Don't allow peers to connect with the regular server
* Added average lines in GUI
## 0.2 - 2024-08-29
* Added support for local discovery of server and peers using UDP port 35483
* The `test` command line option `--latency-peer` is renamed to `--latency-peer-server`.
A new flag `--latency-peer` will instead search for a local peer.
* Improved error messages
* Fix date/time display in remote web page
* Rename the `Latency` tab to `Monitor`
* Change default streams from 16 to 8.
* Change default throughput sample interval from 20 ms to 60 ms.
* Change default load duration from 5 s to 10 s.
* Change default grace duration from 5 s to 10 s.
* Fix serving from link-local interfaces on Linux
* Fix peers on link-local interfaces
* Show download and upload plots for aggregate tests in the GUI
* Added a shortcut (space) to stop the latency monitor
* Change timeout when connecting to servers and peers to 8 seconds
* Added average lines to the plot output
* Show interface IPs when starting servers
## 0.1 - 2024-08-21
* Added `crusader remote` command to start a web server listening on port 35482.
It allows starting tests on a separate machine and
displays the resulting charts in the web page.
* Use system fonts in GUI
* Improved error handling and error messages
* Added `--idle` option to the client to test without traffic
* Save results in a `crusader-results` folder
* Allow building of a server-only binary
* Generated files will use a YYYY-MM-DD HH.MM.SS format
* Rename bandwidth to throughput
* Rename sample rate to sample interval
* Rename `Both` to `Aggregate` and `Total` to `Round-trip` in plots
## 0.0.12 - 2024-07-31
* Create UDP server for each server IP (fixes #22)
* Improved error handling for log messages
* Changed date format to use YYYY-MM-DD in logs
## 0.0.11 - 2024-07-29
* Log file includes timestamps and version number
* Added peer latency measurements
* Added version to title bar of GUI
* Added `plot_max_bandwidth` and `plot_max_latency` command line options
## 0.0.10 - 2024-01-09
* Specify plot title
* Ignore ENOBUFS error
================================================
FILE: LICENSE-APACHE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
================================================
FILE: LICENSE-MIT
================================================
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Crusader Network Tester
[](https://github.com/Zoxc/crusader/releases)
[](https://hub.docker.com/r/zoxc/crusader)
[](https://github.com/Zoxc/crusader/blob/master/LICENSE-MIT)
[](https://github.com/Zoxc/crusader/blob/master/LICENSE-APACHE)

The **Crusader Network Tester** measures network throughput, latency and packet loss
in the presence of upload and download traffic.
It also incorporates a continuous latency tester for
monitoring background responsiveness.
Crusader makes throughput measurements using TCP on port 35481
and latency tests using UDP port 35481.
The remote web server option uses TCP port 35482.
Local server discovery uses UDP port 35483.
**Pre-built binaries** for Windows, Mac, Linux,
and Android are available on the
[Releases](https://github.com/Zoxc/crusader/releases) page.
The GUI is not prebuilt for Linux and must be built from source.
**Documentation** See the [Documentation](#documentation)
section below.
**Status:** The latest Crusader release version is shown above.
The [pre-built binaries](https://github.com/Zoxc/crusader/releases)
always provide the latest version.
See the [CHANGELOG.md](./CHANGELOG.md) file for details.
## Crusader GUI
A test run requires two separate computers,
both running Crusader:
a **server** that listens for connections, and
a **client** that initiates the test.
The Crusader GUI incorporates both the server and
the client and allows you to interact with results.
To use it, download the proper binary from the
[Releases](https://github.com/Zoxc/crusader/releases) page.
When you open the `crusader-gui` you see this window.
Enter the address of another computer that's
running the Crusader server, then click **Start test**.
When the test is complete, the **Result** tab shows a
chart like the second image below.
An easy way to use Crusader is to download
the Crusader GUI onto two computers, then
start the server on one computer, and the client on the other.

The Crusader GUI has five tabs:
* **Client tab**
Runs the Crusader client program.
The options shown above are described in the
[Command-line options](./docs/CLI.md) page.
* **Server tab**
Runs the Crusader server, listening for connections from other clients
* **Remote tab**
Starts a webserver (default port 35482).
A browser that connects to that port can initiate
a test to a Crusader server.
* **Monitor tab**
Continually displays the latency to the selected
Crusader server until stopped.
* **Result tab**
Displays the result of the most recent client run
## The Result Tab

A Crusader test creates three bursts of traffic.
By default, it generates ten seconds each of
download only, upload only, then bi-directional traffic.
Each burst is separated by several seconds of idle time.
The Crusader Result tab displays the results of the test with
three plots (see image above):
* The **Throughput** plot shows the bursts of traffic.
Green is download (from server to client),
blue is upload, and
the purple line is the instantaneous
sum of the download plus upload.
* The **Latency** plot shows the corresponding latency.
Green shows the (uni-directional) time from the server to the client.
Blue is the (uni-directional) time from the client to the server.
Black shows the sum from the client to the server
and back (round-trip time).
* The **Packet Loss** plot has green and blue marks
that indicate times when packets were lost.
For more details, see the
[Understanding Crusader Results](./docs/RESULTS.md) page.
## Documentation
* [This README](./README.md)
* [Understanding Crusader Results](./docs/RESULTS.md)
* [Local Testing](./docs/LOCAL_TESTS.md)
* [Command-line Options](./docs/CLI.md)
* [Building Crusader from source](./docs/BUILDING.md)
* [Troubleshooting](./docs/TROUBLESHOOTING.md)
* [Docker container](https://hub.docker.com/r/zoxc/crusader)
for the server is available on
[dockerhub](https://hub.docker.com/r/zoxc/crusader).
================================================
FILE: android/.gitignore
================================================
.gradle
/target
/app/build
/app/src/main/jniLibs
================================================
FILE: android/Cargo.toml
================================================
[package]
name = "crusader-android"
version = "0.1.0"
edition = "2021"
resolver = "2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
eframe = { version = "0.28.1", features = ["wgpu"] }
crusader-gui-lib = { path = "../src/crusader-gui-lib" }
crusader-lib = { path = "../src/crusader-lib" }
winit = "0.29.15"
jni = "0.19.0"
ndk-context = "0.1"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11.0"
android-activity = { version = "0.5", features = ["game-activity"] }
[patch.crates-io]
winit = { git = "https://github.com/Zoxc/winit", branch = "crusader2" }
egui = { git = "https://github.com/Zoxc/egui", branch = "crusader2" }
epaint = { git = "https://github.com/Zoxc/egui", branch = "crusader2" }
emath = { git = "https://github.com/Zoxc/egui", branch = "crusader2" }
egui_plot = { git = "https://github.com/Zoxc/egui_plot", branch = "crusader" }
[lib]
name = "main"
crate-type = ["cdylib"]
================================================
FILE: android/app/build.gradle
================================================
plugins {
id 'com.android.application'
}
android {
compileSdk 31
defaultConfig {
applicationId "zoxc.crusader"
minSdk 28
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release {
storeFile = file("../../../android.keystore")
storePassword System.getenv("SIGNING_STORE_PASSWORD")
keyAlias System.getenv("SIGNING_KEY_ALIAS")
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
}
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.release
}
debug {
minifyEnabled false
//packagingOptions {
// doNotStrip '**/*.so'
//}
//debuggable true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'com.google.android.material:material:1.5.0'
implementation "androidx.games:games-activity:2.0.2"
// To use the Games Controller Library
//implementation "androidx.games:games-controller:1.1.0"
// To use the Games Text Input Library
//implementation "androidx.games:games-text-input:1.1.0"
}
================================================
FILE: android/app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="zoxc.crusader">
<uses-feature android:glEsVersion="0x30000" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:label="Crusader Network Tester" android:theme="@style/Theme.RustTemplate">
<activity android:name=".MainActivity" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="main" />
</activity>
</application>
</manifest>
================================================
FILE: android/app/src/main/java/zoxc/crusader/MainActivity.java
================================================
package zoxc.crusader;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.google.androidgamesdk.GameActivity;
import android.os.Bundle;
import android.content.pm.PackageManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.util.Log;
import android.content.Intent;
import android.net.Uri;
import android.app.Activity;
import android.view.inputmethod.InputMethodManager;
import android.provider.OpenableColumns;
import android.database.Cursor;
import android.content.Context;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class MainActivity extends GameActivity {
static {
System.loadLibrary("main");
}
public void showKeyboard(boolean show) {
InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
if (show) {
input.showSoftInput(getWindow().getDecorView().getRootView(), 0);
} else {
input.hideSoftInputFromWindow(getWindow().getDecorView().getRootView().getWindowToken(), 0);
}
}
public void loadFile() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, ACTIVITY_LOAD_FILE);
}
public void saveFile(boolean image, String name, byte[] data) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (image) {
intent.setType("image/png");
} else {
intent.setType("application/octet-stream");
}
intent.putExtra(Intent.EXTRA_TITLE, name);
saveFileData = data;
saveImage = image;
startActivityForResult(intent, ACTIVITY_CREATE_FILE);
}
private byte[] saveFileData = null;
private boolean saveImage;
private static final int ACTIVITY_LOAD_FILE = 1;
private static final int ACTIVITY_CREATE_FILE = 2;
static native void fileLoaded(String name, byte[] data);
static native void fileSaved(boolean image, String name);
@Override
public void onActivityResult(int requestCode, int resultCode,
Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == ACTIVITY_CREATE_FILE) {
if (resultCode == Activity.RESULT_OK && resultData != null) {
Uri uri = resultData.getData();
String name = getName(uri);
try {
OutputStream stream = getContentResolver().openOutputStream(uri);
stream.write(saveFileData);
stream.close();
fileSaved(saveImage, name);
}
catch(Exception e) {}
}
saveFileData = null;
}
if (requestCode == ACTIVITY_LOAD_FILE
&& resultCode == Activity.RESULT_OK
&& resultData != null) {
Uri uri = resultData.getData();
String name = getName(uri);
try {
InputStream stream = getContentResolver().openInputStream(uri);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int read;
byte[] byte_buffer = new byte[0x1000];
while ((read = stream.read(byte_buffer, 0, byte_buffer.length)) != -1) {
buffer.write(byte_buffer, 0, read);
}
stream.close();
byte[] data = buffer.toByteArray();
fileLoaded(name, data);
}
catch(Exception e) {}
}
}
public String getName(Uri uri) {
Cursor cursor = getContentResolver().query(uri, null, null, null, null, null);
String name = "";
try {
if (cursor != null && cursor.moveToFirst()) {
int column = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (column != -1) {
name = cursor.getString(column);
}
return name;
}
} finally {
cursor.close();
}
return name;
}
}
================================================
FILE: android/app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="status">#FFE6E6E6</color>
</resources>
================================================
FILE: android/app/src/main/res/values/themes.xml
================================================
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.RustTemplate" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorPrimaryDark">@color/status</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">@color/status</item>
<item name="android:windowLightStatusBar">true</item>
<!-- Customize your theme here. -->
</style>
</resources>
================================================
FILE: android/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.1.2' apply false
id 'com.android.library' version '7.1.2' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: android/debugInstall.ps1
================================================
$ErrorActionPreference = "Stop"
cargo ndk -t arm64-v8a -o app/src/main/jniLibs/ -- build --release
if ($lastexitcode -ne 0) {
throw "Error"
}
./gradlew.bat buildDebug
if ($lastexitcode -ne 0) {
throw "Error"
}
./gradlew.bat installDebug
if ($lastexitcode -ne 0) {
throw "Error"
}
================================================
FILE: android/gradle/wrapper/gradle-wrapper.properties
================================================
#Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
================================================
FILE: android/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
================================================
FILE: android/gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: android/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: android/settings.gradle
================================================
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
include ':app'
================================================
FILE: android/src/lib.rs
================================================
#![allow(
clippy::field_reassign_with_default,
clippy::option_map_unit_fn,
clippy::missing_safety_doc
)]
use crusader_gui_lib::Tester;
use crusader_lib::file_format::RawResult;
use eframe::egui::{self, vec2, Align, FontFamily, Layout};
use jni::{
objects::{JClass, JObject, JString},
sys::{jboolean, jbyteArray},
JNIEnv,
};
use std::{
error::Error,
io::Cursor,
path::Path,
sync::{Arc, Mutex},
};
#[cfg(target_os = "android")]
use {
android_activity::AndroidApp,
crusader_lib::test::PlotConfig,
eframe::{NativeOptions, Renderer, Theme},
log::Level,
std::fs,
winit::platform::android::EventLoopBuilderExtAndroid,
};
struct App {
tester: Tester,
keyboard_shown: bool,
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
use eframe::egui::FontFamily::Proportional;
use eframe::egui::FontId;
use eframe::egui::TextStyle::*;
let mut style = ctx.style();
let style_ = Arc::make_mut(&mut style);
style_.spacing.button_padding = vec2(10.0, 0.0);
style_.spacing.interact_size.y = 40.0;
style_.spacing.item_spacing = vec2(10.0, 10.0);
style_.text_styles = [
(Heading, FontId::new(26.0, Proportional)),
(Body, FontId::new(16.0, Proportional)),
(Monospace, FontId::new(16.0, FontFamily::Monospace)),
(Button, FontId::new(16.0, Proportional)),
(Small, FontId::new(16.0, Proportional)),
]
.into();
ctx.set_style(style);
egui::CentralPanel::default().show(ctx, |ui| {
let mut rect = ui.max_rect();
rect.set_top(rect.top() + 40.0);
rect.set_height(rect.height() - 60.0);
let mut ui = ui.child_ui(rect, Layout::left_to_right(Align::Center), None);
ui.vertical(|ui| {
ui.heading("Crusader Network Benchmark");
ui.separator();
SAVED_FILE.lock().unwrap().take().map(|(image, name)| {
if !image {
self.tester.save_raw(Path::new(&name).to_owned());
}
});
LOADED_FILE.lock().unwrap().take().map(|(name, data)| {
RawResult::load_from_reader(Cursor::new(data))
.map(|data| self.tester.load_file(Path::new(&name).to_owned(), data));
});
self.tester.show(ctx, ui);
});
});
if ctx.wants_keyboard_input() != self.keyboard_shown {
show_keyboard(ctx.wants_keyboard_input()).unwrap();
self.keyboard_shown = ctx.wants_keyboard_input();
}
}
}
fn show_keyboard(show: bool) -> Result<(), Box<dyn Error>> {
let context = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(context.vm().cast())? };
let activity: JObject = (context.context() as jni::sys::jobject).into();
let env = vm.attach_current_thread()?;
env.call_method(activity, "showKeyboard", "(Z)V", &[show.into()])?
.v()?;
Ok(())
}
fn save_file(image: bool, name: String, data: Vec<u8>) -> Result<(), Box<dyn Error>> {
let context = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(context.vm().cast())? };
let activity: JObject = (context.context() as jni::sys::jobject).into();
let env = vm.attach_current_thread()?;
env.call_method(
activity,
"saveFile",
"(ZLjava/lang/String;[B)V",
&[
image.into(),
env.new_string(name).unwrap().into(),
env.byte_array_from_slice(&data).unwrap().into(),
],
)?
.v()?;
Ok(())
}
static SAVED_FILE: Mutex<Option<(bool, String)>> = Mutex::new(None);
#[no_mangle]
pub unsafe extern "C" fn Java_zoxc_crusader_MainActivity_fileSaved(
env: JNIEnv,
_: JClass,
image: jboolean,
name: JString,
) {
let name: String = env.get_string(name).unwrap().into();
*SAVED_FILE.lock().unwrap() = Some((image != 0, name));
}
fn load_file() -> Result<(), Box<dyn Error>> {
let context = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(context.vm().cast())? };
let activity: JObject = (context.context() as jni::sys::jobject).into();
let env = vm.attach_current_thread()?;
env.call_method(activity, "loadFile", "()V", &[])?.v()?;
Ok(())
}
static LOADED_FILE: Mutex<Option<(String, Vec<u8>)>> = Mutex::new(None);
#[no_mangle]
pub unsafe extern "C" fn Java_zoxc_crusader_MainActivity_fileLoaded(
env: JNIEnv,
_: JClass,
name: JString,
data: jbyteArray,
) {
let name: String = env.get_string(name).unwrap().into();
let data = env.convert_byte_array(data).unwrap();
*LOADED_FILE.lock().unwrap() = Some((name, data));
}
#[cfg(target_os = "android")]
#[no_mangle]
fn android_main(app: AndroidApp) {
android_logger::init_once(android_logger::Config::default().with_min_level(Level::Trace));
crusader_lib::plot::register_fonts();
let settings = app
.internal_data_path()
.map(|path| path.join("settings.toml"));
let temp_plot = app.internal_data_path().map(|path| path.join("plot.png"));
let mut options = NativeOptions::default();
options.follow_system_theme = false;
options.default_theme = Theme::Light;
options.renderer = Renderer::Wgpu;
options.event_loop_builder = Some(Box::new(move |builder| {
builder.with_android_app(app.clone());
}));
let mut tester = Tester::new(settings);
tester.file_loader = Some(Box::new(|_| load_file().unwrap()));
tester.plot_saver = Some(Box::new(move |result| {
let path = temp_plot.as_deref().unwrap();
crusader_lib::plot::save_graph_to_path(path, &PlotConfig::default(), result).unwrap();
let data = fs::read(path).unwrap();
fs::remove_file(path).unwrap();
let name = format!("{}.png", crusader_lib::test::timed("plot"));
save_file(true, name, data).unwrap();
}));
tester.raw_saver = Some(Box::new(|result| {
let mut writer = Cursor::new(Vec::new());
result.save_to_writer(&mut writer).unwrap();
let data = writer.into_inner();
let name = format!("{}.crr", crusader_lib::test::timed("data"));
save_file(false, name, data).unwrap();
}));
eframe::run_native(
"Crusader Network Tester",
options,
Box::new(|_cc| {
Ok(Box::new(App {
tester,
keyboard_shown: false,
}))
}),
)
.unwrap();
}
================================================
FILE: docker/README.md
================================================
To build a statically linked server image:
```
docker build .. -t crusader -f server-static.Dockerfile
```
To build a statically linked remote image:
```
docker build .. -t crusader -f remote-static.Dockerfile
```
This image allow initiation of tests using the web application running on port 35482.
Supported platforms:
- `linux/i386`
- `linux/x86_64`
- `linux/arm/v7`
- `linux/arm64`
Available profiles:
- `--build-arg PROFILE=release` (default)
- `--build-arg PROFILE=speed`
- `--build-arg PROFILE=size`
================================================
FILE: docker/remote-static.Dockerfile
================================================
FROM rust AS build
ARG TARGETARCH
ARG PROFILE=release
COPY src /src
WORKDIR /src
RUN echo no-target-detected > /target
RUN if [ "$TARGETARCH" = "386" ]; then\
echo i686-unknown-linux-musl > /target; fi
RUN if [ "$TARGETARCH" = "amd64" ]; then\
echo x86_64-unknown-linux-musl > /target; fi
RUN if [ "$TARGETARCH" = "arm" ]; then\
echo arm-unknown-linux-musleabihf > /target; fi
RUN if [ "$TARGETARCH" = "arm64" ]; then\
echo aarch64-unknown-linux-musl > /target; fi
ENV RUSTFLAGS="-C target-feature=+crt-static"
RUN rustup target add $(cat /target)
RUN cargo build -p crusader --profile=$PROFILE --target $(cat /target)
RUN cp target/$(cat /target)/$PROFILE/crusader /
FROM scratch
COPY --from=build /crusader /
EXPOSE 35482/tcp
ENTRYPOINT [ "/crusader", "remote" ]
================================================
FILE: docker/server-static.Dockerfile
================================================
FROM rust AS build
ARG TARGETARCH
ARG PROFILE=release
COPY src /src
WORKDIR /src
RUN echo no-target-detected > /target
RUN if [ "$TARGETARCH" = "386" ]; then\
echo i686-unknown-linux-musl > /target; fi
RUN if [ "$TARGETARCH" = "amd64" ]; then\
echo x86_64-unknown-linux-musl > /target; fi
RUN if [ "$TARGETARCH" = "arm" ]; then\
echo arm-unknown-linux-musleabihf > /target; fi
RUN if [ "$TARGETARCH" = "arm64" ]; then\
echo aarch64-unknown-linux-musl > /target; fi
ENV RUSTFLAGS="-C target-feature=+crt-static"
RUN rustup target add $(cat /target)
RUN cargo build -p crusader --no-default-features --profile=$PROFILE --target $(cat /target)
RUN cp target/$(cat /target)/$PROFILE/crusader /
FROM scratch
COPY --from=build /crusader /
EXPOSE 35481/tcp 35481/udp 35483/udp
ENTRYPOINT [ "/crusader", "serve" ]
================================================
FILE: docs/BUILDING.md
================================================
# Building Crusader from source
Reminder: [Pre-built binaries](https://github.com/Zoxc/crusader/releases)
are available for everyday tests.
## Required dependencies
* A Rust and C toolchain.
* `fontconfig` (optional, required for `crusader-gui`)
_Note:_ To install `fontconfig` on Ubuntu:
```sh
sudo apt install libfontconfig1-dev
```
## Building Crusader
To develop or debug Crusader, use the commands below
to build the full set of binaries.
Executables are placed in _src/target/release_
To build the `crusader` command line executable:
```sh
cd src
cargo build -p crusader --release
```
To build both command line and GUI executables:
```sh
cd src
cargo build --release
```
## Debug build
Create a debug build by using `cargo build`
(instead of `cargo build --release`).
Binaries are saved in the _src/target/debug_ directory
## Docker
To build a docker container that runs the server:
```sh
cd docker
docker build .. -t crusader -f server-static.Dockerfile
```
================================================
FILE: docs/CLI.md
================================================
# Running Crusader from the command line
## Server
To host a Crusader server, run this on the _server_ machine:
```sh
crusader serve
```
## Client
To start a test, run this on the _client machine_.
See the [command-line options](#options-for-the-test-command) below for details.
```sh
crusader test <server-ip>
```
## Remote
To host a web server that provides remote control of a Crusader client,
run the command below, then connect to
`http://ip-of-the-crusader-device:35482`
```sh
crusader remote
```
## Plot
Crusader creates a `.png` file from a `.crr` file using `crusader plot path-to-crr-file`
The resulting `.png` is saved in the same directory as the input file.
## Export
Crusader exports raw data samples from a `.crr` file
into a `.json` file using `crusader export path-to-crr-file`
The resulting `.json` is saved in the same directory as the input file.
## Options for the `test` command
**Usage: `crusader test [OPTIONS] <SERVER-ADDRESS>`**
**Arguments:** `<SERVER-ADDRESS>` address of a Crusader server
**Options:**
* **`--download`**
Run a download test
* **`--upload`**
Run an upload test
* **`--bidirectional`**
Run a test doing both download and upload
* **`--idle`**
Run a test, only measuring latency without inducing traffic.
The duration is specified by `grace_duration`
* **`--port <PORT>`**
Specify the TCP and UDP port used by the server
[default: 35481]
* **`--streams <STREAMS>`**
The number of TCP connections used to generate
traffic in a single direction
[default: 8]
* **`--stream-stagger <SECONDS>`**
The delay between the start of each stream
[default: 0.0]
* **`--load-duration <SECONDS>`**
The duration in which traffic is generated
[default: 10.0]
* **`--grace-duration <SECONDS>`**
The idle time between each test
[default: 2.0]
* **`--latency-sample-interval <MILLISECONDS>`**
[default: 5.0]
* **`--throughput-sample-interval <MILLISECONDS>`**
[default: 60.0]
* **`--plot-transferred`**
Plot transferred bytes
* **`--plot-split-throughput`**
Plot upload and download separately and plot streams
* **`--plot-max-throughput <BPS>`**
Set the axis for throughput to at least this value.
SI units are supported so `100M` would specify 100 Mbps
* **`--plot-max-latency <MILLISECONDS>`**
Set the axis for latency to at least this value
* **`--plot-width <PIXELS>`**
* **`--plot-height <PIXELS>`**
* **`--plot-title <PLOT_TITLE>`**
* **`--latency-peer-address <LATENCY_PEER_ADDRESS>`**
Address of another Crusader server (the "peer") which
concurrently measures the latency to the server and reports
the values to the client
* **`--latency-peer`**
Trigger the client to instruct a peer (another Crusader server)
to begin measuring the latency to the main server
and report the latency back
* **`--out-name <OUT_NAME>`**
The filename prefix used for the raw data and plot filenames
* **`-h, --help`**
Print help (see a summary with '-h')
================================================
FILE: docs/LOCAL_TESTS.md
================================================
# Local network testing with Crusader
**Background:**
The Crusader Network Tester measures network throughput
and latency in the presence of upload and download traffic
and produces plots of the traffic rates, latency and packet loss.
## Making local tests - Wifi or Wired
To test the equipment between two points on your network,
Crusader requires two computers,
one acting as a server, the other as a client.
The Crusader program can act as both client and server.
Install the latest pre-built binary of
[Crusader](https://github.com/Zoxc/crusader/releases)
on two separate computers. Then:
1. Connect one of those computers to your router's LAN port using an
Ethernet cable.
Start the Crusader program on it, then click the **Server** tab. See the
[screen shot](../media/Crusader-Server.png).
Click **Start server** and look for the
address(es) where the Crusader Server is listening.
(Note: The Crusader binary can also run on a small
computer such as a Raspberry Pi.
A Pi4 acting as a server can easily support 1Gbps speeds.)
2. Connect the other computer either by Ethernet, Wi-fi,
or some other adapter.
Run the Crusader program and click the Client tab. See the
[screen shot](../media/Crusader-Client.png).
Enter the address of a Crusader Server into the GUI,
and click **Start test**
3. When the test completes, you'll see charts of three bursts of traffic:
Download, Upload, and Bidirectional,
along with the latency during that activity.
See the
[Crusader README](../README.md) and
[Understanding Crusader Results](./RESULTS.md)
for details.
**Note:** If both the computers (client and server) are on the LAN
side of the router (whether they are on Wi-Fi or Ethernet),
the results will reflect the _switching_ capability, not the
_routing_ capability of the router.
To test the routing capability, test against a server that's
on the WAN side of the router or the broader Internet.
**Note:** The Crusader program has both a GUI and a command-line binary.
Both act as a client or a server.
These instructions tell how to run the client on a laptop.
You may find it convenient to run the server on a remote computer.
Get both binaries from the
[Releases page](https://github.com/Zoxc/crusader/releases).
## What can we learn?
Your local router, Wi-fi, and other network equipment
all affect your total performance.
Even with very high speed internet service,
if the router is max'ed out it can hold back your throughput.
And you're stuck with whatever latency the router and
other local equipment create - it's added to any latency from your ISP network.
In particular:
* Ethernet-to-Ethernet connections tend to be good:
high throughput with low latency.
But you should always check your network.
On a 1Gbps network, typical results are above 950 Mbps,
and less than a dozen milliseconds of latency.
* Wi-fi has a reputation for "being slow".
This often is a result of the Wi-fi drivers injecting
hundreds of milliseconds of latency.
In addition, wireless connections will always have lower
throughput than wired connections.
* Many powerline adapters (that provide an Ethernet connection
between two AC outlets) may have high specifications,
but in practice are known to give limited throughput
and have high latency.
Running a Crusader test between computers on the local network measures
the performance of that portion of the network.
## Why is this important?
These test results - the actual throughput or latency numbers -
are not very important.
If you are satisfied with you network's performance,
then these numerical results don't matter.
But if you are experiencing problems,
these tests help divide the troubleshooting problem in two.
* If the local network is running fine,
performance problems must be elsewhere
(in your ISP or their upstream service,
which likely is out of your control).
* But if the local performance is not what you expected,
you can start investigating your router, switch, etc.
================================================
FILE: docs/RESULTS.md
================================================
# Understanding Crusader Results
The Crusader GUI provides a compact summary of the test data.
Here are some hints for evaluating the results.
## Result window

Crusader tests the connection using three bursts of traffic.
The Throughput, Latency, and Packet loss are shown in the charts.
In the image above notice:
* **Hovering over a chart** shows crosshairs that give the throughput
or latency of that point in the chart.
In the screen shot above, the Down latency
peaks around 250 ms.
* **Hovering over, or clicking the ⓘ symbol** opens a window that displays
a summary of the measurements.
See the description below for details.
* **Clicking a legend** ("color") in the charts shows/hides
the associated graph.
In the screen shot above, the Latency's "Round-trip" legend has been clicked,
hiding the (black) round-trip trace,
and showing only the Up and Down latency plots.
* The **Save to results** button saves two files: a plot (as `.png`)
and the data (as `.crr`) to the _crusader-results_ directory
in the _current directory_.
* The **Open from results** button opens a `.crr` file
from the _crusader-results_ directory.
* The **Save** button opens a file dialog to save the current `.crr` file.
* The **Open** button opens a file dialog to select a `.crr` file to open.
* The **Export plot** button opens a file dialog to save a `.png` file.
## Numerical Summary Windows
The Crusader GUI displays charts showing Throughput, Latency,
and Packet loss.
The ⓘ symbol opens a window showing a numerical summary of the charted data.
### Throughput
<img src="../media/Crusader-Throughput.png" alt="description" width="250" />
* Download - Steady state throughput, ignoring any startup transients,
during the Download portion of the test
* Upload - Steady state throughput, ignoring any startup transients,
during the Upload portion of the test
* Bidirectional - Sum of the Download and Upload throughputs
during the Bidirectional portion of the test.
Also displays the individual Download and Upload throughputs.
* Streams - number of TCP connections used in each direction
* Stream Stagger - The delay between the start of each stream
* Throughput sample interval - Interval between throughput measurements
### Latency
<img src="../media/Crusader-Latency.png" alt="description" width="250" />
Crusader smooths all the latency samples over a 400 ms window.
The values shown in the window display the maximum of those smoothed values.
This emphasizes sustained peaks of latency.
* Download - Summarizes the round-trip latency during the
Download portion of the test.
Also displays the measured one-way delay for Download (from server to client)
and Upload (client to server)
* Upload - Summarizes the latency for the Upload portion of the test
* Bidirectional - Summarizes the latency for the Bidirectional portion of the test
* Idle latency - Measured latency when no traffic is present.
* Latency sample interval - Interval between latency measurements
### Packet loss
<img src="../media/Crusader-Loss.png" alt="description" width="125" />
When it measures packet loss, Crusader is using the UDP packets
that it also uses for latency measurements.
* Download - Summarizes packet loss during the Download portion of the test
* Upload - Summarizes packet loss during the Upload portion of the test
* Bidirectional - Summarizes packet loss during the Bidirectional
portion of the test
================================================
FILE: docs/TROUBLESHOOTING.md
================================================
# Troubleshooting
* Crusader requires that TCP and UDP ports 35481 are open for its tests.
Crusader also uses ports 35482 for the remote webserver
and port 35483 for discovering other Crusader servers.
Check that your firewall is letting those ports through.
* On macOS, the first time you double-click
the pre-built `crusader` or `crusader-gui` icon,
the OS refuses to let it run.
You must use **System Preferences -> Privacy & Security**
to approve Crusader to run.
* The message
`Warning: Load termination timed out. There may be residual untracked traffic in the background.`
is not necessarily harmful.
It may happen due to the TCP termination being lost
or TCP incompatibilities between OSes.
It's likely benign if you see throughput and latency drop
to idle values after the tests in the graph.
* The up and down latency measurements rely on symmetric stable latency
measurements to the server.
These values may be wrong if those assumption don't hold on test startup.
================================================
FILE: media/Crusader Screen Shots.md
================================================
# Verify Crusader Screen Shots
This page is useful for checking the appearance of screen shots.
Capture the screen shots (Cmd-Shift-5 on macOS) and save with filenames
like "Client.png", "Server.png", etc., one for each of the tabs.
Run the `batch_add_border.sh` script - it finds all these files,
removes the drop shadow and adds "Crusader-" to each result file.
Remove the original files before committing to git.
## Client

## Server

## Remote

## Monitor

## Result

## Result with stats

## Throughput popup

<img src="./Crusader-Throughput.png" alt="description" width="250" />
## Latency popup

<img src="./Crusader-Latency.png" alt="description" width="250" />
## Packet loss popup

<img src="./Crusader-Loss.png" alt="description" width="125" />
================================================
FILE: media/batch_add_border.sh
================================================
#!/bin/bash
# add_border.sh - This script uses ImageMagick to process
# macOS screen shots for publication
# Usage:
# 1. Take screen shots (Cmd-Shift-5) on macOS
# 2. Save the file with a name that would be a good suffix (e.g. Remote.png)
# 3. Run this script. The script outputs modified files prefixed with "Crusader-"
# 4. Discard the original files
# The script does the following - for the .png files in the directory:
# - find all .png files that don't begin with "Crusader"
# - remove the transparent area/drop shadow from a macOS screen shot
# - shrink the image to the size of the image
# - draw a grey border 1 px wide around the window.
# - save the resulting file as "Crusader-....png"
# Thanks, ChatGPT
# Get the directory where the script is located
script_dir="$(dirname "$0")"
# Define the border size (1 pixel) and colors
border_size=1
border_color="gray"
background_color="white"
# Loop through all .png files
for input_file in "$script_dir"/[a-zA-Z]*.png; do
# Check if the file actually exists (in case no files match)
if [ ! -f "$input_file" ]; then
continue
fi
# Ignore files that already start with "Crusader"
if [[ $(basename "$input_file") == Crusader* ]]; then
echo "Skipping: $input_file"
continue
fi
# Output file name (prepend "Crusader-" to the original file name)
output_file="$script_dir/Crusader-$(basename "$input_file")"
# Process the image:
# 1. Remove the alpha channel (transparency) by filling with white
# 2. Trim the image to its non-transparent content
# 3. Add a 1-pixel grey border
magick "$input_file" \
-alpha off \
-trim \
-bordercolor $border_color \
-border ${border_size}x${border_size} \
"$output_file"
echo "Processed: $input_file -> $output_file"
done
echo "All matching .png files processed."
================================================
FILE: src/Cargo.toml
================================================
[workspace]
members = ["crusader", "crusader-lib", "crusader-gui-lib", "crusader-gui"]
resolver = "1"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
strip = "symbols"
[profile.speed]
opt-level = 3
codegen-units = 1
inherits = "release"
lto = "fat"
[profile.size]
inherits = "speed"
opt-level = "z"
[patch.crates-io]
egui_plot = { git = "https://github.com/Zoxc/egui_plot", branch = "crusader" }
================================================
FILE: src/crusader/Cargo.toml
================================================
[package]
name = "crusader"
version = "0.1.0"
edition = "2021"
[dependencies]
crusader-lib = { path = "../crusader-lib" }
clap = { version = "4.5.13", features = ["derive", "string"] }
clap-num = "1.1.1"
env_logger = "0.10.0"
anyhow = "1.0.86"
serde_json = { version = "1.0.122", optional = true }
[features]
default = ["client"]
client = ["crusader-lib/client", "dep:serde_json"]
================================================
FILE: src/crusader/src/main.rs
================================================
use anyhow::Context;
use clap::{Parser, Subcommand};
use clap_num::si_number;
#[cfg(feature = "client")]
use crusader_lib::file_format::RawResult;
#[cfg(feature = "client")]
use crusader_lib::test::PlotConfig;
use crusader_lib::{protocol, version};
#[cfg(feature = "client")]
use crusader_lib::{with_time, Config};
#[cfg(feature = "client")]
use std::path::PathBuf;
use std::process;
#[cfg(feature = "client")]
use {
anyhow::anyhow,
std::fs::OpenOptions,
std::io::{BufWriter, Write},
std::path::Path,
std::time::Duration,
};
#[derive(Parser)]
#[command(version = version())]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Args)]
struct PlotArgs {
#[arg(long, help = "Plot transferred bytes")]
plot_transferred: bool,
#[arg(long, help = "Plot upload and download separately and plot streams")]
plot_split_throughput: bool,
#[arg(long, value_parser=si_number::<u64>, value_name = "BPS",
long_help = "Sets the axis for throughput to at least this value. \
SI units are supported so `100M` would specify 100 Mbps")]
plot_max_throughput: Option<u64>,
#[arg(
long,
value_name = "MILLISECONDS",
help = "Sets the axis for latency to at least this value"
)]
plot_max_latency: Option<u64>,
#[arg(long, value_name = "PIXELS")]
plot_width: Option<u64>,
#[arg(long, value_name = "PIXELS")]
plot_height: Option<u64>,
#[arg(long)]
plot_title: Option<String>,
}
impl PlotArgs {
#[cfg(feature = "client")]
fn config(&self) -> PlotConfig {
PlotConfig {
transferred: self.plot_transferred,
split_throughput: self.plot_split_throughput,
max_throughput: self.plot_max_throughput,
max_latency: self.plot_max_latency,
width: self.plot_width,
height: self.plot_height,
title: self.plot_title.clone(),
}
}
}
#[derive(Subcommand)]
enum Commands {
#[command(about = "Runs the server")]
Serve {
#[arg(long, default_value_t = protocol::PORT, help = "Specifies the TCP and UDP port used by the server")]
port: u16,
#[arg(long, help = "Allow use and discovery as a peer")]
peer: bool,
},
#[command(
long_about = "Runs a test client against a specified server and saves the result to the current directory. \
By default this does a download test, an upload test, and a test doing both download and upload while measuring the latency to the server"
)]
#[cfg(feature = "client")]
Test {
server: Option<String>,
#[arg(long, help = "Run a download test")]
download: bool,
#[arg(long, help = "Run an upload test")]
upload: bool,
#[arg(long, help = "Run a test doing both download and upload")]
bidirectional: bool,
#[arg(
long,
long_help = "Run a test only measuring latency. The duration is specified by `grace_duration`"
)]
idle: bool,
#[arg(long, default_value_t = protocol::PORT, help = "Specifies the TCP and UDP port used by the server")]
port: u16,
#[arg(
long,
default_value_t = 8,
help = "The number of TCP connections used to generate traffic in a single direction"
)]
streams: u64,
#[arg(
long,
default_value_t = 0.0,
value_name = "SECONDS",
help = "The delay between the start of each stream"
)]
stream_stagger: f64,
#[arg(
long,
default_value_t = 10.0,
value_name = "SECONDS",
help = "The duration in which traffic is generated"
)]
load_duration: f64,
#[arg(
long,
default_value_t = 2.0,
value_name = "SECONDS",
help = "The idle time between each test"
)]
grace_duration: f64,
#[arg(long, default_value_t = 5, value_name = "MILLISECONDS")]
latency_sample_interval: u64,
#[arg(long, default_value_t = 60, value_name = "MILLISECONDS")]
throughput_sample_interval: u64,
#[command(flatten)]
plot: PlotArgs,
#[arg(
long,
long_help = "Specifies another server (peer) which will also measure the latency to the server independently of the client"
)]
latency_peer_address: Option<String>,
#[arg(
long,
help = "Use another server (peer) which will also measure the latency to the server independently of the client"
)]
latency_peer: bool,
#[arg(
long,
help = "The filename prefix used for the test result raw data and plot filenames"
)]
out_name: Option<String>,
},
#[cfg(feature = "client")]
#[command(about = "Plots a previous result")]
Plot {
data: PathBuf,
#[command(flatten)]
plot: PlotArgs,
},
#[cfg(feature = "client")]
#[command(about = "Allows the client to be controlled over a web server")]
Remote {
#[arg(
long,
default_value_t = protocol::PORT + 1,
help = "Specifies the HTTP port used by the server"
)]
port: u16,
},
#[cfg(feature = "client")]
#[command(about = "Converts a result file to JSON")]
Export {
data: PathBuf,
#[arg(
long,
short('o'),
help = "The path where the output JSON will be stored"
)]
output: Option<PathBuf>,
#[arg(long, short('f'), help = "Overwrite the file if it exists")]
force: bool,
},
}
fn run() -> Result<(), anyhow::Error> {
let cli = Cli::parse();
match &cli.command {
#[cfg(feature = "client")]
&Commands::Test {
ref server,
download,
upload,
bidirectional,
idle,
throughput_sample_interval,
latency_sample_interval,
ref plot,
port,
streams,
stream_stagger,
grace_duration,
load_duration,
ref latency_peer_address,
latency_peer,
ref out_name,
} => {
let mut config = Config {
port,
streams,
stream_stagger: Duration::from_secs_f64(stream_stagger),
grace_duration: Duration::from_secs_f64(grace_duration),
load_duration: Duration::from_secs_f64(load_duration),
download: !idle,
upload: !idle,
bidirectional: !idle,
ping_interval: Duration::from_millis(latency_sample_interval),
throughput_interval: Duration::from_millis(throughput_sample_interval),
};
if download || upload || bidirectional {
if idle {
println!("Cannot run `idle` test with a load test");
process::exit(1);
}
config.download = download;
config.upload = upload;
config.bidirectional = bidirectional;
}
crusader_lib::test::test(
config,
plot.config(),
server.as_deref(),
(latency_peer || latency_peer_address.is_some())
.then_some(latency_peer_address.as_deref()),
out_name.as_deref().unwrap_or("test"),
)
}
&Commands::Serve { port, peer } => crusader_lib::serve::serve(port, peer),
#[cfg(feature = "client")]
Commands::Remote { port } => crusader_lib::remote::run(*port),
#[cfg(feature = "client")]
Commands::Plot { data, plot } => {
let result = RawResult::load(data).ok_or(anyhow!("Unable to load data"))?;
let root = data.parent().unwrap_or(Path::new(""));
let file = crusader_lib::plot::save_graph(
&plot.config(),
&result.to_test_result(),
data.file_stem()
.and_then(|name| name.to_str())
.unwrap_or("plot"),
data.parent().unwrap_or(Path::new("")),
)?;
println!(
"{}",
with_time(&format!("Saved plot as {}", root.join(file).display()))
);
Ok(())
}
#[cfg(feature = "client")]
Commands::Export {
data,
output,
force,
} => {
let result = RawResult::load(data).ok_or(anyhow!("Unable to load data"))?;
let output = output
.clone()
.unwrap_or_else(|| data.with_extension("json"));
let file = OpenOptions::new()
.create_new(!*force)
.create(*force)
.truncate(true)
.write(true)
.open(output)
.context("Failed to create output file")?;
let mut file = BufWriter::new(file);
serde_json::to_writer_pretty(&mut file, &result).context("Failed to serialize data")?;
file.flush().context("Failed to flush output")?;
Ok(())
}
}
}
fn main() {
env_logger::init();
#[cfg(feature = "client")]
crusader_lib::plot::register_fonts();
if let Err(error) = run() {
println!("Error: {:?}", error);
process::exit(1);
}
}
================================================
FILE: src/crusader-gui/Cargo.toml
================================================
[package]
name = "crusader-gui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
crusader-lib = { path = "../crusader-lib" }
crusader-gui-lib = { path = "../crusader-gui-lib" }
env_logger = "0.10.0"
eframe = "0.28.1"
font-kit = "0.14.2"
================================================
FILE: src/crusader-gui/src/main.rs
================================================
#![allow(
clippy::field_reassign_with_default,
clippy::option_map_unit_fn,
clippy::type_complexity
)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use std::{error::Error, process, sync::Arc};
use crusader_gui_lib::Tester;
use crusader_lib::version;
use eframe::{
egui::{self, Context, FontData, FontDefinitions, FontFamily},
emath::vec2,
Theme,
};
#[allow(unused_imports)]
use font_kit::family_name::FamilyName;
use font_kit::{handle::Handle, properties::Properties, source::SystemSource};
fn main() {
env_logger::init();
let mut options = eframe::NativeOptions::default();
options.follow_system_theme = false;
options.default_theme = Theme::Light;
crusader_lib::plot::register_fonts();
let settings = std::env::current_exe()
.ok()
.map(|exe| exe.with_extension("toml"));
let result = eframe::run_native(
&format!("Crusader Network Tester {}", version()),
options,
Box::new(move |cc| {
let ctx = &cc.egui_ctx;
let mut style = ctx.style();
let style_ = Arc::make_mut(&mut style);
style_.spacing.button_padding = vec2(6.0, 0.0);
style_.spacing.interact_size.y = 30.0;
style_.spacing.item_spacing = vec2(5.0, 5.0);
let font_size = if cfg!(target_os = "macos") {
13.5
} else {
12.5
};
style_.text_styles.get_mut(&egui::TextStyle::Body).map(|v| {
v.size = font_size;
});
style_
.text_styles
.get_mut(&egui::TextStyle::Button)
.map(|v| {
v.size = font_size;
});
style_
.text_styles
.get_mut(&egui::TextStyle::Small)
.map(|v| {
v.size = font_size;
});
style_
.text_styles
.get_mut(&egui::TextStyle::Monospace)
.map(|v| {
v.size = font_size;
});
ctx.set_style(style);
load_system_font(ctx).ok();
Ok(Box::new(App {
tester: Tester::new(settings),
}))
}),
);
if let Err(e) = result {
eprintln!("Failed to run GUI: {}", e);
process::exit(1);
}
}
fn load_system_font(ctx: &Context) -> Result<(), Box<dyn Error>> {
let mut fonts = FontDefinitions::default();
let handle = SystemSource::new().select_best_match(
&[
#[cfg(target_os = "macos")]
FamilyName::SansSerif,
#[cfg(windows)]
FamilyName::Title("Segoe UI".to_string()),
],
&Properties::new(),
)?;
let buf: Vec<u8> = match handle {
Handle::Memory { bytes, .. } => bytes.to_vec(),
Handle::Path { path, .. } => std::fs::read(path)?,
};
const UI_FONT: &str = "System Sans Serif";
fonts
.font_data
.insert(UI_FONT.to_owned(), FontData::from_owned(buf));
if let Some(vec) = fonts.families.get_mut(&FontFamily::Proportional) {
vec.insert(0, UI_FONT.to_owned());
}
if let Some(vec) = fonts.families.get_mut(&FontFamily::Monospace) {
vec.insert(0, UI_FONT.to_owned());
}
ctx.set_fonts(fonts);
Ok(())
}
struct App {
tester: Tester,
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
self.tester.show(ctx, ui);
});
}
}
================================================
FILE: src/crusader-gui-lib/Cargo.toml
================================================
[package]
name = "crusader-gui-lib"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
toml = "0.5.9"
serde = { version = "1.0.137", features = ["derive"] }
crusader-lib = { path = "../crusader-lib", features = ["server", "client"] }
tokio = { version = "1.18.2", features = ["full"] }
eframe = "0.28.1"
egui_plot = "0.28.1"
egui_extras = { version = "0.28.1", default-features = false }
open = "5.3.0"
[target.'cfg(not(target_os = "android"))'.dependencies]
rfd = { version = "0.10.0", default-features = false, features = [
"xdg-portal",
] }
================================================
FILE: src/crusader-gui-lib/src/client.rs
================================================
use crate::{Tab, Tester};
use crusader_lib::{
file_format::RawResult,
protocol,
test::{self},
with_time, Config,
};
use eframe::{
egui::{self, vec2, Grid, ScrollArea, TextEdit, Ui},
emath::Align,
};
use serde::{Deserialize, Serialize};
use std::{mem, sync::Arc, time::Duration};
use tokio::sync::{
mpsc::{self},
oneshot,
};
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(default)]
pub struct ClientSettings {
pub server: String,
pub download: bool,
pub upload: bool,
pub bidirectional: bool,
pub streams: u64,
pub load_duration: f64,
pub grace_duration: f64,
pub stream_stagger: f64,
pub latency_sample_interval: u64,
pub throughput_sample_interval: u64,
pub latency_peer: bool,
pub latency_peer_server: String,
pub advanced: bool,
pub idle_test: bool,
pub idle_duration: f64,
}
impl ClientSettings {
fn config(&self) -> Config {
Config {
port: protocol::PORT,
streams: self.streams,
grace_duration: Duration::from_secs_f64(self.grace_duration),
load_duration: Duration::from_secs_f64(self.load_duration),
stream_stagger: Duration::from_secs_f64(self.stream_stagger),
download: self.download,
upload: self.upload,
bidirectional: self.bidirectional,
ping_interval: Duration::from_millis(self.latency_sample_interval),
throughput_interval: Duration::from_millis(self.throughput_sample_interval),
}
}
}
impl Default for ClientSettings {
fn default() -> Self {
Self {
server: String::new(),
download: true,
upload: true,
bidirectional: true,
streams: 8,
load_duration: 10.0,
grace_duration: 2.0,
stream_stagger: 0.0,
latency_sample_interval: 5,
throughput_sample_interval: 60,
latency_peer: false,
latency_peer_server: String::new(),
advanced: false,
idle_test: false,
idle_duration: 10.0,
}
}
}
pub struct Client {
rx: mpsc::UnboundedReceiver<String>,
pub done: Option<oneshot::Receiver<Option<Result<RawResult, String>>>>,
pub abort: Option<oneshot::Sender<()>>,
}
#[derive(PartialEq, Eq)]
pub enum ClientState {
Stopped,
Stopping,
Running,
}
impl Tester {
fn start_client(&mut self, ctx: &egui::Context) {
self.save_settings();
self.msgs.clear();
self.msg_scrolled = 0;
let (signal_done, done) = oneshot::channel();
let (tx, rx) = mpsc::unbounded_channel();
let ctx = ctx.clone();
let ctx_ = ctx.clone();
let config = if self.settings.client.idle_test {
let mut config = ClientSettings::default().config();
config.grace_duration = Duration::from_secs_f64(self.settings.client.idle_duration);
config.ping_interval =
Duration::from_millis(self.settings.client.latency_sample_interval);
config.bidirectional = false;
config.download = false;
config.upload = false;
config
} else {
self.settings.client.config()
};
let abort = test::test_callback(
config,
(!self.settings.client.server.trim().is_empty())
.then_some(&self.settings.client.server),
self.settings.client.latency_peer.then_some(
(!self.settings.client.latency_peer_server.trim().is_empty())
.then_some(&self.settings.client.latency_peer_server),
),
Arc::new(move |msg| {
tx.send(with_time(msg)).unwrap();
ctx.request_repaint();
}),
Box::new(move |result| {
signal_done.send(result).map_err(|_| ()).unwrap();
ctx_.request_repaint();
}),
);
self.client = Some(Client {
done: Some(done),
rx,
abort: Some(abort),
});
self.client_state = ClientState::Running;
}
fn idle_settings(&mut self, ui: &mut Ui) {
Grid::new("idle-settings").show(ui, |ui| {
ui.label("Duration: ");
ui.add(
egui::DragValue::new(&mut self.settings.client.idle_duration)
.range(0..=1000)
.speed(0.05),
);
ui.label("seconds");
ui.end_row();
if self.settings.client.advanced {
ui.label("Latency sample interval:");
ui.add(
egui::DragValue::new(&mut self.settings.client.latency_sample_interval)
.range(1..=1000)
.speed(0.05),
);
ui.label("milliseconds");
ui.end_row();
}
});
if self.settings.client.advanced {
ui.separator();
ui.horizontal_wrapped(|ui| {
ui.checkbox(&mut self.settings.client.latency_peer, "Latency peer:").on_hover_text("Specifies another server (peer) which will also measure the latency to the server independently of the client");
ui.add_enabled_ui(self.settings.client.latency_peer, |ui| {
ui.add(
TextEdit::singleline(&mut self.settings.client.latency_peer_server)
.hint_text("(Locate local peer)"),
);
});
});
}
ui.separator();
if !self.settings.client.advanced {
let mut any = false;
let config = self.settings.client.clone();
let default = ClientSettings::default();
if config.latency_sample_interval != default.latency_sample_interval {
any = true;
ui.label(format!(
"Latency sample interval: {:.2} milliseconds",
config.latency_sample_interval
));
}
if config.latency_peer != default.latency_peer {
any = true;
let server = (!config.latency_peer_server.trim().is_empty())
.then_some(&*config.latency_peer_server);
ui.label(format!("Latency peer: {}", server.unwrap_or("<Discover>")));
}
if any {
ui.separator();
}
}
}
fn latency_under_load_settings(&mut self, ui: &mut Ui, compact: bool) {
if !self.settings.client.advanced || compact {
ui.horizontal_wrapped(|ui| {
ui.checkbox(&mut self.settings.client.download, "Download")
.on_hover_text("Run a download test");
ui.add_space(10.0);
ui.checkbox(&mut self.settings.client.upload, "Upload")
.on_hover_text("Run an upload test");
ui.add_space(10.0);
ui.checkbox(&mut self.settings.client.bidirectional, "Bidirectional")
.on_hover_text("Run a test doing both download and upload");
});
Grid::new("settings-compact").show(ui, |ui| {
ui.label("Streams: ").on_hover_text(
"The number of TCP connections used to generate traffic in a single direction",
);
ui.add(
egui::DragValue::new(&mut self.settings.client.streams)
.range(1..=1000)
.speed(0.05),
);
ui.end_row();
ui.label("Load duration: ")
.on_hover_text("The duration in which traffic is generated");
ui.add(
egui::DragValue::new(&mut self.settings.client.load_duration)
.range(0..=1000)
.speed(0.05),
);
ui.label("seconds");
ui.end_row();
if self.settings.client.advanced {
ui.label("Grace duration: ")
.on_hover_text("The idle time between each test");
ui.add(
egui::DragValue::new(&mut self.settings.client.grace_duration)
.range(0..=1000)
.speed(0.05),
);
ui.label("seconds");
ui.end_row();
ui.label("Stream stagger: ")
.on_hover_text("The delay between the start of each stream");
ui.add(
egui::DragValue::new(&mut self.settings.client.stream_stagger)
.range(0..=1000)
.speed(0.05),
);
ui.label("seconds");
ui.end_row();
ui.label("Latency sample interval:");
ui.add(
egui::DragValue::new(&mut self.settings.client.latency_sample_interval)
.range(1..=1000)
.speed(0.05),
);
ui.label("milliseconds");
ui.end_row();
ui.label("Throughput sample interval:");
ui.add(
egui::DragValue::new(&mut self.settings.client.throughput_sample_interval)
.range(1..=1000)
.speed(0.05),
);
ui.label("milliseconds");
ui.end_row();
}
});
} else {
Grid::new("settings").show(ui, |ui| {
ui.checkbox(&mut self.settings.client.download, "Download")
.on_hover_text("Run a download test");
ui.allocate_space(vec2(1.0, 1.0));
ui.label("Streams: ").on_hover_text(
"The number of TCP connections used to generate traffic in a single direction",
);
ui.add(
egui::DragValue::new(&mut self.settings.client.streams)
.range(1..=1000)
.speed(0.05),
);
ui.label("");
ui.allocate_space(vec2(1.0, 1.0));
ui.label("Stream stagger: ")
.on_hover_text("The delay between the start of each stream");
ui.add(
egui::DragValue::new(&mut self.settings.client.stream_stagger)
.range(0..=1000)
.speed(0.05),
);
ui.label("seconds");
ui.end_row();
ui.checkbox(&mut self.settings.client.upload, "Upload")
.on_hover_text("Run an upload test");
ui.label("");
ui.label("Load duration: ")
.on_hover_text("The duration in which traffic is generated");
ui.add(
egui::DragValue::new(&mut self.settings.client.load_duration)
.range(0..=1000)
.speed(0.05),
);
ui.label("seconds");
ui.label("");
ui.label("Latency sample interval: ");
ui.add(
egui::DragValue::new(&mut self.settings.client.latency_sample_interval)
.range(1..=1000)
.speed(0.05),
);
ui.label("milliseconds");
ui.end_row();
ui.checkbox(&mut self.settings.client.bidirectional, "Bidirectional")
.on_hover_text("Run a test doing both download and upload");
ui.label("");
ui.label("Grace duration: ")
.on_hover_text("The idle time between each test");
ui.add(
egui::DragValue::new(&mut self.settings.client.grace_duration)
.range(0..=1000)
.speed(0.05),
);
ui.label("seconds");
ui.label("");
ui.label("Throughput sample interval: ");
ui.add(
egui::DragValue::new(&mut self.settings.client.throughput_sample_interval)
.range(1..=1000)
.speed(0.05),
);
ui.label("milliseconds");
ui.end_row();
});
}
if self.settings.client.advanced {
ui.separator();
ui.horizontal_wrapped(|ui| {
ui.checkbox(&mut self.settings.client.latency_peer, "Latency peer:").on_hover_text("Specifies another server (peer) which will also measure the latency to the server independently of the client");
ui.add_enabled_ui(self.settings.client.latency_peer, |ui| {
ui.add(
TextEdit::singleline(&mut self.settings.client.latency_peer_server)
.hint_text("(Locate local peer)"),
);
});
});
}
ui.separator();
if !self.settings.client.advanced {
let mut any = false;
let config = self.settings.client.clone();
let default = ClientSettings::default();
if config.grace_duration != default.grace_duration {
any = true;
ui.label(format!(
"Grace duration: {:.2} seconds",
config.grace_duration
));
}
if config.stream_stagger != default.stream_stagger {
any = true;
ui.label(format!(
"Stream stagger: {:.2} seconds",
config.stream_stagger
));
}
if config.latency_sample_interval != default.latency_sample_interval {
any = true;
ui.label(format!(
"Latency sample interval: {:.2} milliseconds",
config.latency_sample_interval
));
}
if config.throughput_sample_interval != default.throughput_sample_interval {
any = true;
ui.label(format!(
"Throughput sample interval: {:.2} milliseconds",
config.throughput_sample_interval
));
}
if config.latency_peer != default.latency_peer {
any = true;
let server = (!config.latency_peer_server.trim().is_empty())
.then_some(&*config.latency_peer_server);
ui.label(format!("Latency peer: {}", server.unwrap_or("<Discover>")));
}
if any {
ui.separator();
}
}
}
pub fn client(&mut self, ctx: &egui::Context, ui: &mut Ui, compact: bool) {
let active = self.client_state == ClientState::Stopped;
ui.horizontal_wrapped(|ui| {
ui.add_enabled_ui(active, |ui| {
ui.label("Server address:");
let response = ui.add(
TextEdit::singleline(&mut self.settings.client.server)
.hint_text("(Locate local server)"),
);
if self.client_state == ClientState::Stopped
&& response.lost_focus()
&& ui.input(|i| i.key_pressed(egui::Key::Enter))
{
self.start_client(ctx)
}
});
match self.client_state {
ClientState::Running => {
if ui.button("Stop test").clicked() {
let client = self.client.as_mut().unwrap();
mem::take(&mut client.abort).unwrap().send(()).unwrap();
self.client_state = ClientState::Stopping;
}
}
ClientState::Stopping => {
ui.add_enabled_ui(false, |ui| {
let _ = ui.button("Stopping test..");
});
}
ClientState::Stopped => {
if ui.button("Start test").clicked() {
self.start_client(ctx)
}
}
}
});
ui.separator();
ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_enabled_ui(active, |ui| {
ui.horizontal(|ui| {
ui.label("Measure:");
ui.selectable_value(
&mut self.settings.client.idle_test,
false,
"Latency under load",
);
ui.selectable_value(
&mut self.settings.client.idle_test,
true,
"Latency only",
);
});
ui.separator();
if self.settings.client.idle_test {
self.idle_settings(ui);
} else {
self.latency_under_load_settings(ui, compact);
}
ui.horizontal(|ui| {
let mut default = ClientSettings::default();
default.idle_test = self.settings.client.idle_test;
default.advanced = self.settings.client.advanced;
default.server = self.settings.client.server.clone();
default.latency_peer_server =
self.settings.client.latency_peer_server.clone();
let parameters_changed = self.settings.client != default;
ui.add_enabled_ui(parameters_changed, |ui| {
if ui.button("Reset settings").clicked() {
self.settings.client = default;
}
});
ui.toggle_value(&mut self.settings.client.advanced, "Advanced mode");
})
});
if self.client_state == ClientState::Running
|| self.client_state == ClientState::Stopping
{
let client = self.client.as_mut().unwrap();
while let Ok(msg) = client.rx.try_recv() {
println!("[Client] {msg}");
self.msgs.push(msg);
}
if let Ok(result) = client.done.as_mut().unwrap().try_recv() {
match result {
Some(Ok(result)) => {
self.msgs.push(with_time("Test complete"));
let result = result.to_test_result();
self.set_result(result);
if self.tab == Tab::Client {
self.tab = Tab::Result;
}
}
Some(Err(error)) => {
self.msgs.push(with_time(&format!("Error: {error}")));
}
None => {
self.msgs.push(with_time("Aborted..."));
}
}
self.client = None;
self.client_state = ClientState::Stopped;
}
}
if !self.msgs.is_empty() {
ui.separator();
}
for (i, msg) in self.msgs.iter().enumerate() {
let response = ui.label(msg);
if self.msg_scrolled <= i {
self.msg_scrolled = i + 1;
response.scroll_to_me(Some(Align::Max));
}
}
});
}
}
================================================
FILE: src/crusader-gui-lib/src/lib.rs
================================================
#![allow(
clippy::field_reassign_with_default,
clippy::option_map_unit_fn,
clippy::type_complexity,
clippy::too_many_arguments
)]
use std::ffi::OsStr;
use std::hash::Hash;
use std::{
fs, mem,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use client::{Client, ClientSettings, ClientState};
use crusader_lib::plot::{smooth, LatencySummary};
use crusader_lib::test::timed;
use crusader_lib::{
file_format::{RawPing, RawResult, TestKind},
latency,
plot::{self, float_max, to_rates},
protocol, remote, serve,
test::{self, PlotConfig},
with_time,
};
use eframe::egui::{AboveOrBelow, Label, Layout, TextWrapMode};
use eframe::{
egui::{
self, Grid, Id, PopupCloseBehavior, RichText, ScrollArea, TextEdit, TextStyle, Ui, Vec2b,
},
emath::Align,
epaint::Color32,
};
use egui_extras::{Size, Strip, StripBuilder};
use egui_plot::{ColorConflictHandling, Legend, Line, Plot, PlotPoints};
#[cfg(not(target_os = "android"))]
use rfd::FileDialog;
use serde::{Deserialize, Serialize};
use tokio::sync::{
mpsc::{self, error::TryRecvError},
oneshot,
};
mod client;
struct Server {
done: Option<oneshot::Receiver<()>>,
msgs: Vec<String>,
rx: mpsc::UnboundedReceiver<String>,
stop: Option<oneshot::Sender<()>>,
started: oneshot::Receiver<Result<(), String>>,
}
enum ServerState {
Stopped(Option<String>),
Starting,
Stopping,
Running,
}
struct Latency {
done: Option<oneshot::Receiver<Option<Result<(), String>>>>,
abort: Option<oneshot::Sender<()>>,
}
#[derive(PartialEq, Eq)]
enum Tab {
Client,
Server,
Remote,
Monitor,
Result,
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(default)]
pub struct LatencyMonitorSettings {
pub server: String,
pub history: f64,
pub latency_sample_interval: u64,
}
impl Default for LatencyMonitorSettings {
fn default() -> Self {
Self {
server: "".to_owned(),
history: 60.0,
latency_sample_interval: 5,
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(default)]
pub struct Settings {
pub client: ClientSettings,
pub latency_monitor: LatencyMonitorSettings,
}
impl Settings {
fn from_path(path: &Path) -> Self {
fs::read_to_string(path)
.ok()
.and_then(|data| toml::from_str(&data).ok())
.unwrap_or_default()
}
}
pub struct Tester {
tab: Tab,
settings: Settings,
settings_path: Option<PathBuf>,
saved_settings: Settings,
server_state: ServerState,
server: Option<Server>,
remote_state: ServerState,
remote_server: Option<Server>,
client_state: ClientState,
client: Option<Client>,
result_plot_reset: bool,
result: Option<TestResult>,
raw_result_saved: Option<PathBuf>,
open_result: Vec<PathBuf>,
result_name: String,
msgs: Vec<String>,
msg_scrolled: usize,
pub file_loader: Option<Box<dyn Fn(&mut Tester)>>,
pub plot_saver: Option<Box<dyn Fn(&plot::TestResult)>>,
pub raw_saver: Option<Box<dyn Fn(&RawResult)>>,
latency_state: ClientState,
latency: Option<Latency>,
latency_data: Arc<latency::Data>,
latency_stop: Duration,
latency_error: Option<String>,
latency_plot_reset: bool,
}
pub struct LatencyResult {
total: Vec<(f64, f64)>,
max: f64,
up: Vec<(f64, f64)>,
down: Vec<(f64, f64)>,
loss: Vec<(f64, Option<bool>)>,
}
impl LatencyResult {
fn new(result: &plot::TestResult, pings: &[RawPing]) -> Self {
let start = result.start.as_secs_f64();
let total: Vec<_> = pings
.iter()
.filter(|p| p.sent >= result.start)
.filter_map(|p| {
p.latency.and_then(|latency| {
latency
.total
.map(|total| (p.sent.as_secs_f64() - start, total.as_secs_f64() * 1000.0))
})
})
.collect();
let up: Vec<_> = pings
.iter()
.filter(|p| p.sent >= result.start)
.filter_map(|p| {
p.latency.map(|latency| {
(
p.sent.as_secs_f64() - start,
latency.up.as_secs_f64() * 1000.0,
)
})
})
.collect();
let down: Vec<_> = pings
.iter()
.filter(|p| p.sent >= result.start)
.filter_map(|p| {
p.latency.and_then(|latency| {
latency
.down()
.map(|down| (p.sent.as_secs_f64() - start, down.as_secs_f64() * 1000.0))
})
})
.collect();
let loss = pings
.iter()
.filter(|p| p.sent >= result.start)
.filter_map(|ping| {
if ping.latency.and_then(|latency| latency.total).is_none() {
let down_loss =
(result.raw_result.version >= 2).then_some(ping.latency.is_some());
Some((ping.sent.as_secs_f64() - start, down_loss))
} else {
None
}
})
.collect();
let max = float_max(total.iter().map(|v| v.1));
LatencyResult {
total,
up,
down,
loss,
max,
}
}
}
pub struct TestResult {
result: plot::TestResult,
download: Option<Vec<(f64, f64)>>,
download_avg: Option<Vec<(f64, f64)>>,
upload: Option<Vec<(f64, f64)>>,
upload_avg: Option<Vec<(f64, f64)>>,
both_download: Option<Vec<(f64, f64)>>,
both_download_avg: Option<Vec<(f64, f64)>>,
both_upload: Option<Vec<(f64, f64)>>,
both_upload_avg: Option<Vec<(f64, f64)>>,
both: Option<Vec<(f64, f64)>>,
both_avg: Option<Vec<(f64, f64)>>,
local_latency: LatencyResult,
peer_latency: Option<LatencyResult>,
throughput_max: f64,
}
impl TestResult {
fn new(result: plot::TestResult) -> Self {
let smooth_interval =
Duration::from_secs_f64(1.0).min(result.raw_result.config.grace_duration);
let interval = result.raw_result.config.bandwidth_interval;
let start = result.start.as_secs_f64();
let download = result
.download_bytes
.as_ref()
.map(|bytes| handle_bytes(bytes, start));
let download_avg = result
.download_bytes
.as_ref()
.map(|bytes| smooth_bytes(bytes, start, interval, smooth_interval));
let upload = result
.upload_bytes
.as_ref()
.map(|bytes| handle_bytes(bytes, start));
let upload_avg = result
.upload_bytes
.as_ref()
.map(|bytes| smooth_bytes(bytes, start, interval, smooth_interval));
let both_upload = result
.both_upload_bytes
.as_ref()
.map(|bytes| handle_bytes(bytes, start));
let both_upload_avg = result
.both_upload_bytes
.as_ref()
.map(|bytes| smooth_bytes(bytes, start, interval, smooth_interval));
let both_download = result
.both_download_bytes
.as_ref()
.map(|bytes| handle_bytes(bytes, start));
let both_download_avg = result
.both_download_bytes
.as_ref()
.map(|bytes| smooth_bytes(bytes, start, interval, smooth_interval));
let both = result
.both_bytes
.as_ref()
.map(|bytes| handle_bytes(bytes, start));
let both_avg = result
.both_bytes
.as_ref()
.map(|bytes| smooth_bytes(bytes, start, interval, smooth_interval));
let download_max = download
.as_ref()
.map(|data| float_max(data.iter().map(|v| v.1)));
let upload_max = upload
.as_ref()
.map(|data| float_max(data.iter().map(|v| v.1)));
let both_upload_max = both_upload
.as_ref()
.map(|data| float_max(data.iter().map(|v| v.1)));
let both_download_max = both_download
.as_ref()
.map(|data| float_max(data.iter().map(|v| v.1)));
let both_max = both
.as_ref()
.map(|data| float_max(data.iter().map(|v| v.1)));
let throughput_max = float_max(
[
download_max,
upload_max,
both_upload_max,
both_download_max,
both_max,
]
.into_iter()
.flatten(),
);
TestResult {
download,
download_avg,
upload,
upload_avg,
both_download,
both_download_avg,
both_upload,
both_upload_avg,
both,
both_avg,
throughput_max,
local_latency: LatencyResult::new(&result, &result.pings),
peer_latency: result
.raw_result
.peer_pings
.as_ref()
.map(|pings| LatencyResult::new(&result, pings)),
result,
}
}
}
pub fn handle_bytes(data: &[(u64, f64)], start: f64) -> Vec<(f64, f64)> {
to_rates(data)
.into_iter()
.map(|(time, speed)| (Duration::from_micros(time).as_secs_f64() - start, speed))
.collect()
}
pub fn smooth_bytes(
data: &[(u64, f64)],
start: f64,
interval: Duration,
smoothing_interval: Duration,
) -> Vec<(f64, f64)> {
smooth(data, interval, smoothing_interval)
.into_iter()
.map(|(time, speed)| (Duration::from_micros(time).as_secs_f64() - start, speed))
.collect()
}
fn hover_popup(
ui: &mut Ui,
id_source: impl Hash,
position: AboveOrBelow,
popup: impl FnOnce(&mut Ui),
) {
ui.scope(|ui| {
let id = ui.make_persistent_id(id_source);
ui.spacing_mut().interact_size.y = 18.0;
let active = id.with("active");
let active_value = ui.memory_mut(|mem| {
let active = mem.data.get_temp_mut_or_default(active);
*active
});
let style = ui.style_mut();
style.visuals.widgets.inactive.rounding = 50.0.into();
style.visuals.widgets.hovered.rounding = 50.0.into();
style.visuals.widgets.active.rounding = 50.0.into();
style.visuals.widgets.inactive.fg_stroke.color = Color32::from_gray(140);
if active_value {
style.visuals.widgets.inactive.weak_bg_fill = style.visuals.selection.bg_fill;
style.visuals.widgets.hovered.weak_bg_fill = style.visuals.selection.bg_fill;
}
let stats = ui.button("i");
let popup_id = id.with("popup");
let contains_pointer =
if let Some(pointer) = ui.ctx().input(|input| input.pointer.latest_pos()) {
stats.interact_rect.expand(5.0).contains(pointer)
} else {
false
};
if stats.hovered() && contains_pointer {
ui.memory_mut(|mem| {
if !mem.any_popup_open() {
mem.open_popup(popup_id);
}
});
} else if !active_value && !contains_pointer {
ui.memory_mut(|mem| {
if mem.is_popup_open(popup_id) {
mem.close_popup();
}
});
}
if stats.clicked() {
ui.memory_mut(|mem| {
let active: &mut bool = mem.data.get_temp_mut_or_default(active);
*active = !*active;
if *active {
mem.open_popup(popup_id);
} else {
mem.close_popup();
}
});
}
egui::popup::popup_above_or_below_widget(
ui,
popup_id,
&stats,
position,
PopupCloseBehavior::CloseOnClickOutside,
|ui| {
popup(ui);
},
);
ui.memory_mut(|mem| {
if !mem.is_popup_open(popup_id) {
let active: &mut bool = mem.data.get_temp_mut_or_default(active);
if *active {
// ui.ctx().request_repaint();
}
*active = false;
}
});
});
}
impl Drop for Tester {
fn drop(&mut self) {
self.save_settings();
// Stop client
self.client.as_mut().map(|client| {
mem::take(&mut client.abort).map(|abort| {
abort.send(()).unwrap();
});
mem::take(&mut client.done).map(|done| {
done.blocking_recv().ok();
});
});
// Stop server
self.server.as_mut().map(|server| {
mem::take(&mut server.stop).map(|stop| {
stop.send(()).unwrap();
});
mem::take(&mut server.done).map(|done| {
done.blocking_recv().ok();
});
});
// Stop latency
self.latency.as_mut().map(|latency| {
mem::take(&mut latency.abort).map(|abort| {
abort.send(()).unwrap();
});
mem::take(&mut latency.done).map(|done| {
done.blocking_recv().ok();
});
});
}
}
impl Tester {
pub fn new(settings_path: Option<PathBuf>) -> Tester {
let settings = settings_path
.as_deref()
.map_or(Settings::default(), Settings::from_path);
Tester {
tab: Tab::Client,
saved_settings: settings.clone(),
settings,
settings_path,
client_state: ClientState::Stopped,
client: None,
result: None,
result_plot_reset: false,
raw_result_saved: None,
result_name: "".to_string(),
open_result: Vec::new(),
msgs: Vec::new(),
msg_scrolled: 0,
server_state: ServerState::Stopped(None),
server: None,
remote_state: ServerState::Stopped(None),
remote_server: None,
file_loader: None,
raw_saver: None,
plot_saver: None,
latency_state: ClientState::Stopped,
latency: None,
latency_data: Arc::new(latency::Data::new(0, Arc::new(|| {}))),
latency_stop: Duration::from_secs(0),
latency_error: None,
latency_plot_reset: false,
}
}
pub fn set_result(&mut self, result: plot::TestResult) {
self.result = Some(TestResult::new(result));
self.result_name = "test".to_owned();
self.result_plot_reset = true;
self.raw_result_saved = None;
}
pub fn load_file(&mut self, name: PathBuf, raw: RawResult) {
self.set_result(raw.to_test_result());
self.raw_result_saved = Some(name);
}
pub fn save_raw(&mut self, name: PathBuf) {
self.raw_result_saved = Some(name);
}
fn save_settings(&mut self) {
if self.settings != self.saved_settings {
self.settings_path.as_deref().map(|path| {
toml::ser::to_string_pretty(&self.settings)
.map(|data| fs::write(path, data.as_bytes()))
.ok();
});
self.saved_settings = self.settings.clone();
}
}
fn load_result(&mut self) {
#[cfg(not(target_os = "android"))]
{
FileDialog::new()
.add_filter("Crusader Raw Result", &["crr"])
.add_filter("All files", &["*"])
.pick_file()
.map(|file| {
RawResult::load(&file).map(|raw| {
self.load_file(file, raw);
})
});
}
let file_loader = self.file_loader.take();
file_loader.as_ref().map(|loader| loader(self));
self.file_loader = file_loader;
}
fn latency_and_loss(
&mut self,
strip: &mut Strip<'_, '_>,
link: Id,
reset: bool,
peer: bool,
y_axis_size: f32,
) {
let result = self.result.as_ref().unwrap();
let data = if peer {
result.peer_latency.as_ref().unwrap()
} else {
&result.local_latency
};
let latencies = if peer {
&result.result.peer_latencies
} else {
&result.result.latencies
};
let duration = result.result.duration.as_secs_f64() * 1.1;
strip.cell(|ui| {
ui.horizontal(|ui| {
let label = if peer { "Peer latency" } else { "Latency" };
ui.label(label);
hover_popup(
ui,
(label, "Popup"),
if !peer && result.result.raw_result.idle() {
AboveOrBelow::Below
} else {
AboveOrBelow::Above
},
|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.spacing_mut().interact_size.y = 10.0;
let stats = |ui: &mut Ui, name, color, latency: &LatencySummary| {
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(RichText::new(format!("{name}: ")).color(color));
ui.label(format!(
"{:.01} ms",
latency.total.as_secs_f64() * 1000.0
));
});
ui.horizontal(|ui| {
ui.label(format!(
"\t\t{:.01} ms ",
latency.down.as_secs_f64() * 1000.0
));
ui.label(
RichText::new("down").color(Color32::from_rgb(95, 145, 62)),
);
});
ui.horizontal(|ui| {
ui.label(format!(
"\t\t{:.01} ms ",
latency.up.as_secs_f64() * 1000.0
));
ui.label(
RichText::new("up").color(Color32::from_rgb(37, 83, 169)),
);
});
});
};
if let Some(latency) = latencies.latencies.get(&Some(TestKind::Download)) {
stats(ui, "Download", Color32::from_rgb(95, 145, 62), latency);
}
if let Some(latency) = latencies.latencies.get(&Some(TestKind::Upload)) {
stats(ui, "Upload", Color32::from_rgb(37, 83, 169), latency);
}
if let Some(latency) =
latencies.latencies.get(&Some(TestKind::Bidirectional))
{
stats(
ui,
"Bidirectional",
Color32::from_rgb(149, 96, 153),
latency,
);
}
if let Some(latency) = latencies.latencies.get(&None) {
stats(ui, "Latency", Color32::from_rgb(0, 0, 0), latency);
}
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(
RichText::new("Idle latency: ")
.color(Color32::from_rgb(128, 128, 128)),
);
ui.label(format!(
"{:.02} ms",
result.result.raw_result.server_latency.as_secs_f64() * 1000.0
));
});
});
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(
RichText::new("Latency sample interval: ")
.color(Color32::from_rgb(128, 128, 128)),
);
ui.label(format!(
"{:.02} ms",
result.result.raw_result.config.ping_interval.as_secs_f64()
* 1000.0
));
});
});
},
);
});
// Latency
let mut plot = Plot::new((peer, "ping"))
.legend(Legend::default().insertion_order(true))
.y_axis_min_width(y_axis_size)
.link_axis(link, true, false)
.link_cursor(link, true, false)
.include_x(0.0)
.include_x(duration)
.include_y(0.0)
.include_y(data.max * 1.1)
.label_formatter(|_, value| {
format!("Latency = {:.2} ms\nTime = {:.2} s", value.y, value.x)
});
if reset {
plot = plot.reset();
}
plot.show(ui, |plot_ui| {
if result.result.raw_result.version >= 1 {
let latency = data.up.iter().map(|v| [v.0, v.1]);
let latency = Line::new(PlotPoints::from_iter(latency))
.color(Color32::from_rgb(37, 83, 169))
.name("Up");
plot_ui.line(latency);
let latency = data.down.iter().map(|v| [v.0, v.1]);
let latency = Line::new(PlotPoints::from_iter(latency))
.color(Color32::from_rgb(95, 145, 62))
.name("Down");
plot_ui.line(latency);
}
let latency = data.total.iter().map(|v| [v.0, v.1]);
let latency = Line::new(PlotPoints::from_iter(latency))
.color(Color32::from_rgb(50, 50, 50))
.name("Round-trip");
plot_ui.line(latency);
});
});
strip.cell(|ui| {
ui.horizontal(|ui| {
let label = if peer {
"Peer packet loss"
} else {
"Packet loss"
};
ui.label(label);
hover_popup(ui, (label, "Popup"), AboveOrBelow::Above, |ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.spacing_mut().interact_size.y = 10.0;
let stats = |ui: &mut Ui, name, color, (down, up): (f64, f64)| {
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(RichText::new(format!("{name}: ")).color(color));
if down == 0.0 && up == 0.0 {
ui.label("0%");
} else {
ui.label(format!(
"{:.1$}% ",
down * 100.0,
if down == 0.0 { 0 } else { 2 }
));
ui.label(
RichText::new("down").color(Color32::from_rgb(95, 145, 62)),
);
ui.label(format!(
", {:.1$}% ",
up * 100.0,
if up == 0.0 { 0 } else { 2 }
));
ui.label(
RichText::new("up").color(Color32::from_rgb(37, 83, 169)),
);
}
});
});
};
if let Some(loss) = latencies.loss.get(&Some(TestKind::Download)) {
stats(ui, "Download", Color32::from_rgb(95, 145, 62), *loss);
}
if let Some(loss) = latencies.loss.get(&Some(TestKind::Upload)) {
stats(ui, "Upload", Color32::from_rgb(37, 83, 169), *loss);
}
if let Some(loss) = latencies.loss.get(&Some(TestKind::Bidirectional)) {
stats(ui, "Bidirectional", Color32::from_rgb(149, 96, 153), *loss);
}
if let Some(loss) = latencies.loss.get(&None) {
stats(ui, "Packet loss", Color32::from_rgb(0, 0, 0), *loss);
}
});
});
// Packet loss
let mut plot = Plot::new((peer, "loss"))
.legend(Legend::default())
.show_axes([false, true])
.show_grid(Vec2b::new(true, false))
.y_axis_min_width(y_axis_size)
.y_axis_formatter(|_, _| String::new())
.link_axis(link, true, false)
.link_cursor(link, true, false)
.center_y_axis(true)
.allow_zoom(false)
.allow_boxed_zoom(false)
.include_x(0.0)
.include_x(duration)
.include_y(-1.0)
.include_y(1.0)
.height(30.0)
.label_formatter(|_, value| format!("Time = {:.2} s", value.x));
if reset {
plot = plot.reset();
}
plot.show(ui, |plot_ui| {
for &(loss, down_loss) in &data.loss {
let (color, s, e) = down_loss
.map(|down_loss| {
if down_loss {
(Color32::from_rgb(95, 145, 62), 1.0, 0.0)
} else {
(Color32::from_rgb(37, 83, 169), -1.0, 0.0)
}
})
.unwrap_or((Color32::from_rgb(193, 85, 85), -1.0, 1.0));
plot_ui.line(
Line::new(PlotPoints::from_iter(
[[loss, s], [loss, e]].iter().copied(),
))
.color(color),
);
if down_loss.is_some() {
plot_ui.line(
Line::new(PlotPoints::from_iter(
[[loss, s], [loss, s - s / 5.0]].iter().copied(),
))
.width(3.0)
.color(color),
);
}
}
});
});
}
fn load_popup(&mut self, ui: &mut Ui) {
if cfg!(not(target_os = "android")) {
ui.add_space(10.0);
let popup_id = ui.make_persistent_id("Load-Popup");
let button = ui.button("Open from results");
if button.clicked() {
ui.memory_mut(|mem| {
mem.toggle_popup(popup_id);
if mem.is_popup_open(popup_id) {
self.open_result = fs::read_dir("crusader-results")
.ok()
.map(|dir| {
dir.filter_map(|file| {
file.ok()
.map(|file| file.path())
.filter(|path| path.extension() == Some(OsStr::new("crr")))
})
.collect()
})
.unwrap_or_default();
}
});
}
egui::popup::popup_below_widget(
ui,
popup_id,
&button,
PopupCloseBehavior::CloseOnClickOutside,
|ui| {
ui.set_min_width(300.0);
ui.horizontal_wrapped(|ui| {
ui.label("Results available in the");
if ui.link("crusader-results").clicked() {
open::that("crusader-results").ok();
}
ui.label("folder:");
});
ScrollArea::vertical().show(ui, |ui| {
ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
for file in self.open_result.clone() {
if let Some(prefix) =
file.file_name().and_then(|stem| stem.to_str())
{
if ui.toggle_value(&mut false, prefix).clicked() {
ui.memory_mut(|mem| mem.close_popup());
RawResult::load(&file).map(|raw| {
self.load_file(file, raw);
});
}
}
}
});
});
},
);
}
}
fn result(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
if self.result.is_none() {
ui.horizontal_wrapped(|ui| {
if ui.button("Open").clicked() {
self.load_result();
}
self.load_popup(ui);
});
ui.separator();
ui.label("No result.");
return;
}
ui.horizontal_wrapped(|ui| {
if ui.button("Open").clicked() {
self.load_result();
}
if ui.button("Save").clicked() {
match self.raw_saver.as_ref() {
Some(saver) => {
saver(&self.result.as_ref().unwrap().result.raw_result);
}
None => {
#[cfg(not(target_os = "android"))]
{
FileDialog::new()
.add_filter("Crusader Raw Result", &["crr"])
.add_filter("All files", &["*"])
.set_file_name(&format!("{}.crr", timed("test")))
.save_file()
.map(|file| {
if self
.result
.as_ref()
.unwrap()
.result
.raw_result
.save(&file)
.is_ok()
{
self.raw_result_saved = Some(file);
}
});
}
}
}
}
self.load_popup(ui);
if cfg!(not(target_os = "android")) {
let popup_id = ui.make_persistent_id("Save-Popup");
let button = ui.button("Save to results");
if button.clicked() {
ui.memory_mut(|mem| {
mem.toggle_popup(popup_id);
});
}
egui::popup::popup_below_widget(
ui,
popup_id,
&button,
PopupCloseBehavior::CloseOnClickOutside,
|ui| {
ui.set_min_width(250.0);
ui.horizontal_wrapped(|ui| {
ui.label("This saves both the data and plot in the");
if ui.link("crusader-results").clicked() {
open::that("crusader-results").ok();
}
ui.label("folder.");
});
ui.horizontal(|ui| {
ui.label("Name: ");
let mut click = ui
.add(
TextEdit::singleline(&mut self.result_name)
.desired_width(175.0),
)
.lost_focus()
&& ui.input(|i| i.key_pressed(egui::Key::Enter));
click |= ui.button("Save").clicked();
if click {
let name = timed(&self.result_name);
self.raw_result_saved = test::save_raw(
&self.result.as_ref().unwrap().result.raw_result,
&name,
Path::new("crusader-results"),
)
.ok();
plot::save_graph(
&PlotConfig::default(),
&self.result.as_ref().unwrap().result,
&name,
Path::new("crusader-results"),
)
.ok();
ui.memory_mut(|mem| {
mem.close_popup();
});
}
});
},
);
}
ui.add_space(10.0);
if ui.button("Export plot").clicked() {
match self.plot_saver.as_ref() {
Some(saver) => {
saver(&self.result.as_ref().unwrap().result);
}
None => {
#[cfg(not(target_os = "android"))]
{
let name = self
.raw_result_saved
.as_ref()
.and_then(|file| {
file.file_stem()
.unwrap_or_default()
.to_str()
.map(|s| s.to_owned())
})
.unwrap_or(timed("test"));
let mut dialog = FileDialog::new()
.add_filter("Portable Network Graphics", &["png"])
.add_filter("All files", &["*"])
.set_file_name(&format!("{}.png", name));
if let Some(file) = self.raw_result_saved.as_ref() {
if let Some(parent) = file.parent() {
dialog = dialog.set_directory(parent);
}
}
dialog.save_file().map(|file| {
if plot::save_graph_to_path(
&file,
&PlotConfig::default(),
&self.result.as_ref().unwrap().result,
)
.is_ok()
{
file.file_name()
.unwrap_or_default()
.to_str()
.map(|s| s.to_owned());
}
});
}
}
}
}
});
ui.separator();
self.raw_result_saved
.as_ref()
.and_then(|file| {
file.file_name()
.unwrap_or_default()
.to_str()
.map(|s| s.to_owned())
})
.map(|file| {
ui.label(format!("Saved as: {file}"));
ui.separator();
});
let result = self.result.as_ref().unwrap();
if result.result.raw_result.server_overload {
ui.label("Warning: Server overload detected during test. Result should be discarded.");
ui.separator();
}
if result.result.raw_result.load_termination_timeout {
ui.label("Warning: Load termination timed out. There may be residual untracked traffic in the background.");
ui.separator();
}
let packet_loss_size = 75.0;
let result = self.result.as_ref().unwrap();
let link = ui.id().with("result-link");
let mut strip = StripBuilder::new(ui);
if result.result.raw_result.streams() > 0 {
strip = strip.size(Size::remainder());
}
for _ in 0..(1 + result.peer_latency.is_some() as u8) {
strip = strip
.size(Size::remainder())
.size(Size::exact(packet_loss_size));
}
strip.vertical(|mut strip| {
let reset = mem::take(&mut self.result_plot_reset);
let result = self.result.as_ref().unwrap();
let y_axis_size = 30.0;
let duration = result.result.duration.as_secs_f64() * 1.1;
if result.result.raw_result.streams() > 0 {
strip.cell(|ui| {
ui.horizontal(|ui| {
ui.label("Throughput");
hover_popup(ui, "Throughput-Popup", AboveOrBelow::Below, |ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.spacing_mut().interact_size.y = 10.0;
if let Some(throughput) = result
.result
.throughputs
.get(&(TestKind::Download, TestKind::Download))
{
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(
RichText::new("Download: ")
.color(Color32::from_rgb(95, 145, 62)),
);
ui.label(format!("{:.02} Mbps", throughput));
});
});
}
if let Some(throughput) = result
.result
.throughputs
.get(&(TestKind::Upload, TestKind::Upload))
{
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(
RichText::new("Upload: ")
.color(Color32::from_rgb(37, 83, 169)),
);
ui.label(format!("{:.02} Mbps", throughput));
});
});
}
if let Some(throughput) = result
.result
.throughputs
.get(&(TestKind::Bidirectional, TestKind::Bidirectional))
{
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(
RichText::new("Bidirectional: ")
.color(Color32::from_rgb(149, 96, 153)),
);
ui.label(format!("{:.02} Mbps ", throughput));
});
if let Some(down) = result
.result
.throughputs
.get(&(TestKind::Bidirectional, TestKind::Download))
{
if let Some(up) = result
.result
.throughputs
.get(&(TestKind::Bidirectional, TestKind::Upload))
{
ui.horizontal(|ui| {
ui.label(format!("\t\t{:.02} Mbps ", down));
ui.label(
RichText::new("down")
.color(Color32::from_rgb(95, 145, 62)),
);
});
ui.horizontal(|ui| {
ui.label(format!("\t\t{:.02} Mbps ", up));
ui.label(
RichText::new("up")
.color(Color32::from_rgb(37, 83, 169)),
);
});
}
}
});
}
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(
RichText::new("Streams: ")
.color(Color32::from_rgb(128, 128, 128)),
);
ui.label(format!("{}", result.result.raw_result.streams()));
});
});
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(
RichText::new("Stream Stagger: ")
.color(Color32::from_rgb(128, 128, 128)),
);
ui.label(format!(
"{:.02} seconds",
result.result.raw_result.config.stagger.as_secs_f64()
));
});
});
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(
RichText::new("Throughput sample interval: ")
.color(Color32::from_rgb(128, 128, 128)),
);
ui.label(format!(
"{:.02} ms",
result
.result
.raw_result
.config
.bandwidth_interval
.as_secs_f64()
* 1000.0
));
});
});
});
});
// Throughput
let mut plot = Plot::new("result")
.legend(
Legend::default()
.color_conflict_handling(ColorConflictHandling::PickFirst)
.insertion_order(true),
)
.y_axis_min_width(y_axis_size)
.link_axis(link, true, false)
.link_cursor(link, true, false)
.include_x(0.0)
.include_x(duration)
.include_y(0.0)
.include_y(result.throughput_max * 1.1)
.height(ui.available_height())
.label_formatter(|_, value| {
format!("Throughput = {:.2} Mbps\nTime = {:.2} s", value.y, value.x)
});
if reset {
plot = plot.reset();
}
plot.show(ui, |plot_ui| {
let width = 1.0;
if let Some(data) = result.download.as_ref() {
let download = data.iter().map(|v| [v.0, v.1]);
let download = Line::new(PlotPoints::from_iter(download))
.color(Color32::from_rgb(95, 145, 62))
.width(width)
.name("Download");
plot_ui.line(download);
}
if let Some(data) = result.upload.as_ref() {
let upload = data.iter().map(|v| [v.0, v.1]);
let upload = Line::new(PlotPoints::from_iter(upload))
.color(Color32::from_rgb(37, 83, 169))
.width(width)
.name("Upload");
plot_ui.line(upload);
}
if let Some(data) = result.both_download.as_ref() {
let download = data.iter().map(|v| [v.0, v.1]);
let download = Line::new(PlotPoints::from_iter(download))
.color(Color32::from_rgb(95, 145, 62))
.width(width)
.name("Download");
plot_ui.line(download);
}
if let Some(data) = result.both_upload.as_ref() {
let upload = data.iter().map(|v| [v.0, v.1]);
let upload = Line::new(PlotPoints::from_iter(upload))
.color(Color32::from_rgb(37, 83, 169))
.width(width)
.name("Upload");
plot_ui.line(upload);
}
if let Some(data) = result.both.as_ref() {
let both = data.iter().map(|v| [v.0, v.1]);
let both = Line::new(PlotPoints::from_iter(both))
.color(Color32::from_rgb(149, 96, 153))
.width(width)
.name("Aggregate");
plot_ui.line(both);
}
// Average lines
let darken = 0.5;
let alpha = 0.35;
if let Some(data) = result.download_avg.as_ref() {
let download = data.iter().map(|v| [v.0, v.1]);
let download = Line::new(PlotPoints::from_iter(download))
.color(
Color32::from_rgb(95, 145, 62)
.lerp_to_gamma(Color32::BLACK, darken)
.gamma_multiply(alpha),
)
.allow_hover(false)
.width(3.5)
.name("Download");
plot_ui.line(download);
}
if let Some(data) = result.upload_avg.as_ref() {
let upload = data.iter().map(|v| [v.0, v.1]);
let upload = Line::new(PlotPoints::from_iter(upload))
.color(
Color32::from_rgb(37, 83, 169)
.lerp_to_gamma(Color32::BLACK, darken)
.gamma_multiply(alpha),
)
.allow_hover(false)
.width(3.5)
.name("Upload");
plot_ui.line(upload);
}
if let Some(data) = result.both_download_avg.as_ref() {
let download = data.iter().map(|v| [v.0, v.1]);
let download = Line::new(PlotPoints::from_iter(download))
.color(
Color32::from_rgb(95, 145, 62)
.lerp_to_gamma(Color32::BLACK, darken)
.gamma_multiply(alpha),
)
.allow_hover(false)
.width(3.5)
.name("Download");
plot_ui.line(download);
}
if let Some(data) = result.both_upload_avg.as_ref() {
let upload = data.iter().map(|v| [v.0, v.1]);
let upload = Line::new(PlotPoints::from_iter(upload))
.color(
Color32::from_rgb(37, 83, 169)
.lerp_to_gamma(Color32::BLACK, darken)
.gamma_multiply(alpha),
)
.allow_hover(false)
.width(3.5)
.name("Upload");
plot_ui.line(upload);
}
if let Some(data) = result.both_avg.as_ref() {
let both = data.iter().map(|v| [v.0, v.1]);
let both = Line::new(PlotPoints::from_iter(both))
.color(
Color32::from_rgb(149, 96, 153)
.lerp_to_gamma(Color32::BLACK, darken)
.gamma_multiply(alpha),
)
.allow_hover(false)
.width(3.5)
.name("Aggregate");
plot_ui.line(both);
}
});
})
}
self.latency_and_loss(&mut strip, link, reset, false, y_axis_size);
let result = self.result.as_ref().unwrap();
if result.peer_latency.is_some() {
self.latency_and_loss(&mut strip, link, reset, true, y_axis_size);
}
});
}
fn server(&mut self, ctx: &egui::Context, ui: &mut Ui) {
match self.server_state {
ServerState::Stopped(ref error) => {
let (server_button, peer_button) = ui
.horizontal_wrapped(|ui| (ui.button("Start server"), ui.button("Start peer")))
.inner;
if let Some(error) = error {
ui.separator();
ui.label(format!("Unable to start server: {}", error));
}
if server_button.clicked() || peer_button.clicked() {
let ctx = ctx.clone();
let ctx_ = ctx.clone();
let ctx__ = ctx.clone();
let (tx, rx) = mpsc::unbounded_channel();
let (signal_started, started) = oneshot::channel();
let (signal_done, done) = oneshot::channel();
let stop = serve::serve_until(
protocol::PORT,
peer_button.clicked(),
Box::new(move |msg| {
tx.send(with_time(msg)).ok();
ctx.request_repaint();
}),
Box::new(move |result| {
signal_started.send(result).ok();
ctx_.request_repaint();
}),
Box::new(move || {
signal_done.send(()).ok();
ctx__.request_repaint();
}),
)
.ok();
if let Some(stop) = stop {
self.server = Some(Server {
done: Some(done),
stop: Some(stop),
started,
rx,
msgs: Vec::new(),
});
self.server_state = ServerState::Starting;
}
};
ui.separator();
ui.label(format!(
"A server listens on TCP and UDP port {}. It allows clients \
to run tests and measure latency against it. It can also act as a latency peer for tests connecting to another server.",
protocol::PORT
));
}
ServerState::Running => {
let server = self.server.as_mut().unwrap();
let button = ui.button("Stop server");
ui.separator();
loop {
match server.rx.try_recv() {
Ok(msg) => {
println!("[Server] {msg}");
server.msgs.push(msg);
}
Err(TryRecvError::Disconnected) => panic!(),
Err(TryRecvError::Empty) => break,
}
}
ScrollArea::vertical()
.stick_to_bottom(true)
.auto_shrink([false; 2])
.show_rows(
ui,
ui.text_style_height(&TextStyle::Body),
server.msgs.len(),
|ui, rows| {
for row in rows {
ui.label(&server.msgs[row]);
}
},
);
if button.clicked() {
mem::take(&mut server.stop).unwrap().send(()).unwrap();
self.server_state = ServerState::Stopping;
};
}
ServerState::Starting => {
let server = self.server.as_mut().unwrap();
if let Ok(result) = server.started.try_recv() {
if let Err(error) = result {
self.server_state = ServerState::Stopped(Some(error));
self.server = None;
} else {
self.server_state = ServerState::Running;
}
}
ui.add_enabled_ui(false, |ui| {
let _ = ui.button("Starting..");
});
}
ServerState::Stopping => {
if let Ok(()) = self
.server
.as_mut()
.unwrap()
.done
.as_mut()
.unwrap()
.try_recv()
{
self.server_state = ServerState::Stopped(None);
self.server = None;
}
ui.add_enabled_ui(false, |ui| {
let _ = ui.button("Stopping..");
});
}
}
}
fn remote(&mut self, ctx: &egui::Context, ui: &mut Ui) {
match self.remote_state {
ServerState::Stopped(ref error) => {
let button = ui
.vertical(|ui| {
let button = ui.button("Start server");
if let Some(error) = error {
ui.separator();
ui.label(format!("Unable to start server: {}", error));
}
button
})
.inner;
if button.clicked() {
let ctx = ctx.clone();
let ctx_ = ctx.clone();
let ctx__ = ctx.clone();
let (tx, rx) = mpsc::unbounded_channel();
let (signal_started, started) = oneshot::channel();
let (signal_done, done) = oneshot::channel();
let stop = remote::serve_until(
protocol::PORT + 1,
Box::new(move |msg| {
tx.send(with_time(msg)).ok();
ctx.request_repaint();
}),
Box::new(move |result| {
signal_started.send(result).ok();
ctx_.request_repaint();
}),
Box::new(move || {
signal_done.send(()).ok();
ctx__.request_repaint();
}),
)
.ok();
if let Some(stop) = stop {
self.remote_server = Some(Server {
done: Some(done),
stop: Some(stop),
started,
rx,
msgs: Vec::new(),
});
self.remote_state = ServerState::Starting;
}
};
ui.separator();
ui.label(format!(
"A remote server runs a web server on TCP port {}. It allows web clients to remotely start \
tests against other servers.",
protocol::PORT + 1
));
}
ServerState::Running => {
let remote_server = self.remote_server.as_mut().unwrap();
let button = ui.button("Stop server");
ui.separator();
loop {
match remote_server.rx.try_recv() {
Ok(msg) => {
println!("[Remote] {msg}");
remote_server.msgs.push(msg);
}
Err(TryRecvError::Disconnected) => panic!(),
Err(TryRecvError::Empty) => break,
}
}
ScrollArea::vertical()
.stick_to_bottom(true)
.auto_shrink([false; 2])
.show_rows(
ui,
ui.text_style_height(&TextStyle::Body),
remote_server.msgs.len(),
|ui, rows| {
for row in rows {
ui.label(&remote_server.msgs[row]);
}
},
);
if button.clicked() {
mem::take(&mut remote_server.stop)
.unwrap()
.send(())
.unwrap();
self.remote_state = ServerState::Stopping;
};
}
ServerState::Starting => {
let remote_server = self.remote_server.as_mut().unwrap();
if let Ok(result) = remote_server.started.try_recv() {
if let Err(error) = result {
self.remote_state = ServerState::Stopped(Some(error));
self.remote_server = None;
} else {
self.remote_state = ServerState::Running;
}
}
ui.add_enabled_ui(false, |ui| {
let _ = ui.button("Starting..");
});
}
ServerState::Stopping => {
if let Ok(()) = self
.remote_server
.as_mut()
.unwrap()
.done
.as_mut()
.unwrap()
.try_recv()
{
self.remote_state = ServerState::Stopped(None);
self.remote_server = None;
}
ui.add_enabled_ui(false, |ui| {
let _ = ui.button("Stopping..");
});
}
}
}
fn start_monitor(&mut self, ctx: &egui::Context) {
self.save_settings();
let (signal_done, done) = oneshot::channel();
let ctx_ = ctx.clone();
let data = Arc::new(latency::Data::new(
((self.settings.latency_monitor.history * 1000.0)
/ self.settings.latency_monitor.latency_sample_interval as f64)
.round() as usize,
Arc::new(move || {
ctx_.request_repaint();
}),
));
let ctx_ = ctx.clone();
let abort = latency::test_callback(
latency::Config {
port: protocol::PORT,
ping_interval: Duration::from_millis(
self.settings.latency_monitor.latency_sample_interval,
),
},
(!self.settings.latency_monitor.server.trim().is_empty())
.then_some(&self.settings.latency_monitor.server),
data.clone(),
Box::new(move |result| {
signal_done.send(result).map_err(|_| ()).unwrap();
ctx_.request_repaint();
}),
);
self.latency = Some(Latency {
done: Some(done),
abort: Some(abort),
});
self.latency_state = ClientState::Running;
self.latency_data = data;
self.latency_error = None;
self.latency_plot_reset = true;
}
fn monitor(&mut self, ctx: &egui::Context, ui: &mut Ui) {
let running = self.latency_state != ClientState::Stopped;
if !running {
ui.horizontal_wrapped(|ui| {
ui.label("Server address:");
let response = ui.add(
TextEdit::singleline(&mut self.settings.latency_monitor.server)
.hint_text("(Locate local server)"),
);
let enter = response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
if ui.button("Start test").clicked() || enter {
self.start_monitor(ctx)
}
});
}
if running {
ui.horizontal(|ui| {
match self.latency_state {
ClientState::Running => {
if ui.button("Stop test").clicked()
|| ui.input(|i| i.key_pressed(egui::Key::Space))
{
let latency = self.latency.as_mut().unwrap();
mem::take(&mut latency.abort).unwrap().send(()).unwrap();
self.latency_state = ClientState::Stopping;
}
}
ClientState::Stopping => {
ui.add_enabled_ui(false, |ui| {
let _ = ui.button("Stopping test..");
});
}
ClientState::Stopped => {}
}
let state = match *self.latency_data.state.lock() {
latency::State::Connecting => "Connecting..".to_owned(),
latency::State::Monitoring { ref at } => format!("Connected to {at}"),
latency::State::Syncing => "Synchronizing clocks..".to_owned(),
};
ui.add(Label::new(state).wrap_mode(TextWrapMode::Truncate));
let latency = self.latency.as_mut().unwrap();
if let Ok(result) = latency.done.as_mut().unwrap().try_recv() {
self.latency_error = match result {
Some(Ok(())) => None,
Some(Err(error)) => Some(error),
None => Some("Aborted...".to_owned()),
};
self.latency_stop = self.latency_data.start.elapsed();
self.latency = None;
self.latency_state = ClientState::Stopped;
}
});
}
ui.separator();
ui.add_enabled_ui(!running, |ui| {
Grid::new("latency-settings-compact").show(ui, |ui| {
ui.label("History: ");
ui.add(
egui::DragValue::new(&mut self.settings.latency_monitor.history)
.range(0..=1000)
.speed(0.05),
);
ui.label("seconds");
ui.end_row();
ui.label("Latency sample interval:");
ui.add(
egui::DragValue::new(
&mut self.settings.latency_monitor.latency_sample_interval,
)
.range(1..=1000)
.speed(0.05),
);
ui.label("milliseconds");
});
});
ui.separator();
if let Some(error) = self.latency_error.as_ref() {
ui.label(format!("Error: {}", error));
ui.separator();
}
self.latency_data(ctx, ui);
}
fn latency_data(&mut self, ctx: &egui::Context, ui: &mut Ui) {
ui.vertical(|ui| {
let packet_loss_size = 80.0;
let height = ui.available_height();
let duration = self.settings.latency_monitor.history;
let points = self.latency_data.points.blocking_lock().clone();
let now = if self.latency_state == ClientState::Running {
ctx.request_repaint();
self.latency_data.start.elapsed()
} else {
self.latency_stop
}
.as_secs_f64();
let reset = mem::take(&mut self.latency_plot_reset);
let link = ui.id().with("latency-link");
let y_axis_size = 30.0;
// Latency
let mut plot = Plot::new("latency-ping")
.legend(Legend::default().insertion_order(true))
.link_axis(link, true, false)
.link_cursor(link, true, false)
.include_x(-duration)
.include_x(0.0)
.include_x(duration * 0.20)
.include_y(0.0)
.include_y(10.0)
.height(height - packet_loss_size)
.y_axis_min_width(y_axis_size)
.auto_bounds(Vec2b::new(false, true))
.label_formatter(|_, value| {
format!("Latency = {:.2} ms\nTime = {:.2} s", value.y, value.x)
});
if reset {
plot = plot.reset();
}
ui.label("Latency");
plot.show(ui, |plot_ui| {
let latency = points.iter().filter_map(|point| {
point.up.map(|up| {
let up = if let Some(total) = point.total {
up.min(total)
} else {
up
};
[point.sent.as_secs_f64() - now, 1000.0 * up.as_secs_f64()]
})
});
let latency = Line::new(PlotPoints::from_iter(latency))
.color(Color32::from_rgb(37, 83, 169))
.name("Up");
plot_ui.line(latency);
let latency = points.iter().filter_map(|point| {
point
.up
.and_then(|up| point.total.map(|total| total.saturating_sub(up)))
.map(|down| [point.sent.as_secs_f64() - now, 1000.0 * down.as_secs_f64()])
});
let latency = Line::new(PlotPoints::from_iter(latency))
.color(Color32::from_rgb(95, 145, 62))
.name("Down");
plot_ui.line(latency);
let latency = points.iter().filter_map(|point| {
point
.total
.map(|total| [point.sent.as_secs_f64() - now, 1000.0 * total.as_secs_f64()])
});
let latency = Line::new(PlotPoints::from_iter(latency))
.color(Color32::from_rgb(50, 50, 50))
.name("Round-trip");
plot_ui.line(latency);
});
// Packet loss
let mut plot = Plot::new("latency-loss")
.legend(Legend::default())
.show_axes([false, true])
.show_grid(Vec2b::new(true, false))
.y_axis_min_width(y_axis_size)
.y_axis_formatter(|_, _| String::new())
.link_axis(link, true, false)
.link_cursor(link, true, false)
.center_y_axis(true)
.allow_zoom(false)
.allow_boxed_zoom(false)
.include_x(-duration)
.include_x(0.0)
.include_x(duration * 0.15)
.include_y(-1.0)
.include_y(1.0)
.height(30.0)
.label_formatter(|_, value| format!("Time = {:.2} s", value.x));
if reset {
plot = plot.reset();
}
ui.label("Packet loss");
plot.show(ui, |plot_ui| {
let loss = points
.iter()
.filter(|point| !point.pending && point.total.is_none());
for point in loss {
let loss = point.sent.as_secs_f64() - now;
let (color, s, e) = if point.up.is_some() {
(Color32::from_rgb(95, 145, 62), 1.0, 0.0)
} else {
(Color32::from_rgb(37, 83, 169), -1.0, 0.0)
};
plot_ui.line(
Line::new(PlotPoints::from_iter(
[[loss, s], [loss, e]].iter().copied(),
))
.color(color),
);
plot_ui.line(
Line::new(PlotPoints::from_iter(
[[loss, s], [loss, s - s / 5.0]].iter().copied(),
))
.width(3.0)
.color(color),
);
}
});
});
}
pub fn show(&mut self, ctx: &egui::Context, ui: &mut Ui) {
ctx.input(|input| {
if let Some(file) = input
.raw
.dropped_files
.first()
.and_then(|file| file.path.as_deref())
{
RawResult::load(file).map(|raw| {
self.load_file(file.to_owned(), raw);
self.tab = Tab::Result;
});
}
});
let compact = ui.available_width() < 660.0;
ui.horizontal_wrapped(|ui| {
ui.selectable_value(&mut self.tab, Tab::Client, "Client");
ui.selectable_value(&mut self.tab, Tab::Server, "Server");
ui.selectable_value(&mut self.tab, Tab::Remote, "Remote");
ui.selectable_value(&mut self.tab, Tab::Monitor, "Monitor");
ui.selectable_value(&mut self.tab, Tab::Result, "Result");
});
ui.separator();
match self.tab {
Tab::Client => self.client(ctx, ui, compact),
Tab::Server => self.server(ctx, ui),
Tab::Remote => self.remote(ctx, ui),
Tab::Monitor => self.monitor(ctx, ui),
Tab::Result => self.result(ctx, ui),
}
}
}
================================================
FILE: src/crusader-lib/Cargo.toml
================================================
[package]
name = "crusader-lib"
version = "0.1.0"
edition = "2021"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
server = []
client = ["dep:plotters", "dep:axum", "dep:image", "dep:snap", "dep:serde_json"]
[dependencies]
plotters = { version = "0.3.6", default-features = false, optional = true, features = [
"ab_glyph",
"bitmap_backend",
"line_series",
"bitmap_encoder",
] }
chrono = "0.4.19"
bincode = "1.3.3"
serde = { version = "1.0.137", features = ["derive"] }
serde_json = { version = "1.0.122", optional = true }
rand = "0.8.5"
parking_lot = "0.12.0"
hostname = "0.4.0"
tokio = { version = "1.18.2", features = ["full"] }
tokio-util = { version = "0.7.2", features = ["codec"] }
futures = "0.3.21"
bytes = "1.1.0"
snap = { version = "1.0.5", optional = true }
rmp-serde = "1.1.0"
socket2 = "0.4.6"
nix = { version = "0.29.0", features = ["net"] }
libc = "0.2"
anyhow = "1.0.86"
axum = { version = "0.7.5", features = [
"ws",
"tokio",
"http1",
], default-features = false, optional = true }
image = { version = "0.24.9", optional = true }
[target."cfg(target_os = \"windows\")".dependencies]
ipconfig = { version = "=0.3.2", default-features = false }
widestring = "=1.1.0"
================================================
FILE: src/crusader-lib/UFL.txt
================================================
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.
================================================
FILE: src/crusader-lib/assets/vue.js
================================================
/**
* vue v3.4.35
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/
/*! #__NO_SIDE_EFFECTS__ */
// @__NO_SIDE_EFFECTS__
function makeMap(str, expectsLowerCase) {
const set = new Set(str.split(","));
return expectsLowerCase ? (val) => set.has(val.toLowerCase()) : (val) => set.has(val);
}
const EMPTY_OBJ = Object.freeze({}) ;
const EMPTY_ARR = Object.freeze([]) ;
const NOOP = () => {
};
const NO = () => false;
const isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter
(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97);
const isModelListener = (key) => key.startsWith("onUpdate:");
const extend = Object.assign;
const remove = (arr, el) => {
const i = arr.indexOf(el);
if (i > -1) {
arr.splice(i, 1);
}
};
const hasOwnProperty$1 = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty$1.call(val, key);
const isArray = Array.isArray;
const isMap = (val) => toTypeString(val) === "[object Map]";
const isSet = (val) => toTypeString(val) === "[object Set]";
const isDate = (val) => toTypeString(val) === "[object Date]";
const isRegExp = (val) => toTypeString(val) === "[object RegExp]";
const isFunction = (val) => typeof val === "function";
const isString = (val) => typeof val === "string";
const isSymbol = (val) => typeof val === "symbol";
const isObject = (val) => val !== null && typeof val === "object";
const isPromise = (val) => {
return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch);
};
const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
const toRawType = (value) => {
return toTypeString(value).slice(8, -1);
};
const isPlainObject = (val) => toTypeString(val) === "[object Object]";
const isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key;
const isReservedProp = /* @__PURE__ */ makeMap(
// the leading comma is intentional so empty string "" is also included
",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"
);
const isBuiltInDirective = /* @__PURE__ */ makeMap(
"bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"
);
const cacheStringFunction = (fn) => {
const cache = /* @__PURE__ */ Object.create(null);
return (str) => {
const hit = cache[str];
return hit || (cache[str] = fn(str));
};
};
const camelizeRE = /-(\w)/g;
const camelize = cacheStringFunction((str) => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : "");
});
const hyphenateRE = /\B([A-Z])/g;
const hyphenate = cacheStringFunction(
(str) => str.replace(hyphenateRE, "-$1").toLowerCase()
);
const capitalize = cacheStringFunction((str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
});
const toHandlerKey = cacheStringFunction((str) => {
const s = str ? `on${capitalize(str)}` : ``;
return s;
});
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);
const invokeArrayFns = (fns, ...arg) => {
for (let i = 0; i < fns.length; i++) {
fns[i](...arg);
}
};
const def = (obj, key, value, writable = false) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
writable,
value
});
};
const looseToNumber = (val) => {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
const toNumber = (val) => {
const n = isString(val) ? Number(val) : NaN;
return isNaN(n) ? val : n;
};
let _globalThis;
const getGlobalThis = () => {
return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {});
};
const PatchFlagNames = {
[1]: `TEXT`,
[2]: `CLASS`,
[4]: `STYLE`,
[8]: `PROPS`,
[16]: `FULL_PROPS`,
[32]: `NEED_HYDRATION`,
[64]: `STABLE_FRAGMENT`,
[128]: `KEYED_FRAGMENT`,
[256]: `UNKEYED_FRAGMENT`,
[512]: `NEED_PATCH`,
[1024]: `DYNAMIC_SLOTS`,
[20
gitextract_275plfem/
├── .dockerignore
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── release.md
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── android/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── app/
│ │ ├── build.gradle
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── zoxc/
│ │ │ └── crusader/
│ │ │ └── MainActivity.java
│ │ └── res/
│ │ └── values/
│ │ ├── colors.xml
│ │ └── themes.xml
│ ├── build.gradle
│ ├── debugInstall.ps1
│ ├── gradle/
│ │ └── wrapper/
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── settings.gradle
│ └── src/
│ └── lib.rs
├── data/
│ ├── v0.crr
│ ├── v1.crr
│ └── v2.crr
├── docker/
│ ├── README.md
│ ├── remote-static.Dockerfile
│ └── server-static.Dockerfile
├── docs/
│ ├── BUILDING.md
│ ├── CLI.md
│ ├── LOCAL_TESTS.md
│ ├── RESULTS.md
│ └── TROUBLESHOOTING.md
├── media/
│ ├── Crusader Screen Shots.md
│ └── batch_add_border.sh
└── src/
├── Cargo.toml
├── crusader/
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
├── crusader-gui/
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
├── crusader-gui-lib/
│ ├── Cargo.toml
│ └── src/
│ ├── client.rs
│ └── lib.rs
└── crusader-lib/
├── Cargo.toml
├── UFL.txt
├── assets/
│ ├── vue.js
│ └── vue.prod.js
├── build.rs
└── src/
├── common.rs
├── discovery.rs
├── file_format.rs
├── latency.rs
├── lib.rs
├── peer.rs
├── plot.rs
├── protocol.rs
├── remote.html
├── remote.rs
├── serve.rs
└── test.rs
SYMBOL INDEX (1414 symbols across 20 files)
FILE: android/app/src/main/java/zoxc/crusader/MainActivity.java
class MainActivity (line 30) | public class MainActivity extends GameActivity {
method showKeyboard (line 35) | public void showKeyboard(boolean show) {
method loadFile (line 44) | public void loadFile() {
method saveFile (line 51) | public void saveFile(boolean image, String name, byte[] data) {
method fileLoaded (line 71) | static native void fileLoaded(String name, byte[] data);
method fileSaved (line 73) | static native void fileSaved(boolean image, String name);
method onActivityResult (line 75) | @Override
method getName (line 117) | public String getName(Uri uri) {
FILE: android/src/lib.rs
type App (line 32) | struct App {
method update (line 38) | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
function show_keyboard (line 91) | fn show_keyboard(show: bool) -> Result<(), Box<dyn Error>> {
function save_file (line 105) | fn save_file(image: bool, name: String, data: Vec<u8>) -> Result<(), Box...
function Java_zoxc_crusader_MainActivity_fileSaved (line 127) | pub unsafe extern "C" fn Java_zoxc_crusader_MainActivity_fileSaved(
function load_file (line 137) | fn load_file() -> Result<(), Box<dyn Error>> {
function Java_zoxc_crusader_MainActivity_fileLoaded (line 149) | pub unsafe extern "C" fn Java_zoxc_crusader_MainActivity_fileLoaded(
function android_main (line 162) | fn android_main(app: AndroidApp) {
FILE: src/crusader-gui-lib/src/client.rs
type ClientSettings (line 21) | pub struct ClientSettings {
method config (line 40) | fn config(&self) -> Config {
method default (line 57) | fn default() -> Self {
type Client (line 78) | pub struct Client {
type ClientState (line 85) | pub enum ClientState {
method start_client (line 92) | fn start_client(&mut self, ctx: &egui::Context) {
method idle_settings (line 142) | fn idle_settings(&mut self, ui: &mut Ui) {
method latency_under_load_settings (line 206) | fn latency_under_load_settings(&mut self, ui: &mut Ui, compact: bool) {
method client (line 411) | pub fn client(&mut self, ctx: &egui::Context, ui: &mut Ui, compact: bool) {
FILE: src/crusader-gui-lib/src/lib.rs
type Server (line 50) | struct Server {
type ServerState (line 58) | enum ServerState {
type Latency (line 65) | struct Latency {
type Tab (line 71) | enum Tab {
type LatencyMonitorSettings (line 81) | pub struct LatencyMonitorSettings {
method default (line 88) | fn default() -> Self {
type Settings (line 99) | pub struct Settings {
method from_path (line 105) | fn from_path(path: &Path) -> Self {
type Tester (line 113) | pub struct Tester {
method new (line 478) | pub fn new(settings_path: Option<PathBuf>) -> Tester {
method set_result (line 512) | pub fn set_result(&mut self, result: plot::TestResult) {
method load_file (line 519) | pub fn load_file(&mut self, name: PathBuf, raw: RawResult) {
method save_raw (line 524) | pub fn save_raw(&mut self, name: PathBuf) {
method save_settings (line 528) | fn save_settings(&mut self) {
method load_result (line 539) | fn load_result(&mut self) {
method latency_and_loss (line 557) | fn latency_and_loss(
method load_popup (line 844) | fn load_popup(&mut self, ui: &mut Ui) {
method result (line 907) | fn result(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
method server (line 1414) | fn server(&mut self, ctx: &egui::Context, ui: &mut Ui) {
method remote (line 1543) | fn remote(&mut self, ctx: &egui::Context, ui: &mut Ui) {
method start_monitor (line 1676) | fn start_monitor(&mut self, ctx: &egui::Context) {
method monitor (line 1718) | fn monitor(&mut self, ctx: &egui::Context, ui: &mut Ui) {
method latency_data (line 1812) | fn latency_data(&mut self, ctx: &egui::Context, ui: &mut Ui) {
method show (line 1956) | pub fn show(&mut self, ctx: &egui::Context, ui: &mut Ui) {
type LatencyResult (line 143) | pub struct LatencyResult {
method new (line 151) | fn new(result: &plot::TestResult, pings: &[RawPing]) -> Self {
type TestResult (line 214) | pub struct TestResult {
method new (line 232) | fn new(result: plot::TestResult) -> Self {
function handle_bytes (line 334) | pub fn handle_bytes(data: &[(u64, f64)], start: f64) -> Vec<(f64, f64)> {
function smooth_bytes (line 341) | pub fn smooth_bytes(
function hover_popup (line 353) | fn hover_popup(
method drop (line 442) | fn drop(&mut self) {
FILE: src/crusader-gui/src/main.rs
function main (line 21) | fn main() {
function load_system_font (line 88) | fn load_system_font(ctx: &Context) -> Result<(), Box<dyn Error>> {
type App (line 125) | struct App {
method update (line 130) | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
FILE: src/crusader-lib/assets/vue.js
function makeMap (line 8) | function makeMap(str, expectsLowerCase) {
constant EMPTY_OBJ (line 13) | const EMPTY_OBJ = Object.freeze({}) ;
constant EMPTY_ARR (line 14) | const EMPTY_ARR = Object.freeze([]) ;
constant GLOBALS_ALLOWED (line 128) | const GLOBALS_ALLOWED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloa...
function generateCodeFrame (line 132) | function generateCodeFrame(source, start = 0, end = source.length) {
function normalizeStyle (line 173) | function normalizeStyle(value) {
function parseStringStyle (line 193) | function parseStringStyle(cssText) {
function stringifyStyle (line 203) | function stringifyStyle(styles) {
function normalizeClass (line 217) | function normalizeClass(value) {
function normalizeProps (line 237) | function normalizeProps(props) {
constant HTML_TAGS (line 249) | const HTML_TAGS = "html,body,base,head,link,meta,style,title,address,art...
constant SVG_TAGS (line 250) | const SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clip...
constant MATH_TAGS (line 251) | const MATH_TAGS = "annotation,annotation-xml,maction,maligngroup,malignm...
constant VOID_TAGS (line 252) | const VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,s...
function includeBooleanAttr (line 263) | function includeBooleanAttr(value) {
function isRenderableAttrValue (line 272) | function isRenderableAttrValue(value) {
function looseCompareArrays (line 280) | function looseCompareArrays(a, b) {
function looseEqual (line 288) | function looseEqual(a, b) {
function looseIndexOf (line 326) | function looseIndexOf(arr, val) {
function warn$2 (line 369) | function warn$2(msg, ...args) {
class EffectScope (line 374) | class EffectScope {
method constructor (line 375) | constructor(detached = false) {
method active (line 396) | get active() {
method run (line 399) | run(fn) {
method on (line 416) | on() {
method off (line 423) | off() {
method stop (line 426) | stop(fromParent) {
function effectScope (line 452) | function effectScope(detached) {
function recordEffectScope (line 455) | function recordEffectScope(effect, scope = activeEffectScope) {
function getCurrentScope (line 460) | function getCurrentScope() {
function onScopeDispose (line 463) | function onScopeDispose(fn) {
class ReactiveEffect (line 474) | class ReactiveEffect {
method constructor (line 475) | constructor(fn, trigger, scheduler, scope) {
method dirty (line 503) | get dirty() {
method dirty (line 523) | set dirty(v) {
method run (line 526) | run() {
method stop (line 546) | stop() {
function triggerComputed (line 555) | function triggerComputed(computed) {
function preCleanupEffect (line 558) | function preCleanupEffect(effect2) {
function postCleanupEffect (line 562) | function postCleanupEffect(effect2) {
function cleanupDepEffect (line 570) | function cleanupDepEffect(dep, effect2) {
function effect (line 579) | function effect(fn, options) {
function stop (line 599) | function stop(runner) {
function pauseTracking (line 605) | function pauseTracking() {
function resetTracking (line 609) | function resetTracking() {
function pauseScheduling (line 613) | function pauseScheduling() {
function resetScheduling (line 616) | function resetScheduling() {
function trackEffect (line 622) | function trackEffect(effect2, dep, debuggerEventExtraInfo) {
function triggerEffects (line 641) | function triggerEffects(dep, dirtyLevel, debuggerEventExtraInfo) {
constant ITERATE_KEY (line 674) | const ITERATE_KEY = Symbol("iterate" );
constant MAP_KEY_ITERATE_KEY (line 675) | const MAP_KEY_ITERATE_KEY = Symbol("Map key iterate" );
function track (line 676) | function track(target, type, key) {
function trigger (line 697) | function trigger(target, type, key, newValue, oldValue, oldTarget) {
function getDepFromReactive (line 761) | function getDepFromReactive(object, key) {
function createArrayInstrumentations (line 771) | function createArrayInstrumentations() {
function hasOwnProperty (line 799) | function hasOwnProperty(key) {
class BaseReactiveHandler (line 805) | class BaseReactiveHandler {
method constructor (line 806) | constructor(_isReadonly = false, _isShallow = false) {
method get (line 810) | get(target, key, receiver) {
class MutableReactiveHandler (line 854) | class MutableReactiveHandler extends BaseReactiveHandler {
method constructor (line 855) | constructor(isShallow2 = false) {
method set (line 858) | set(target, key, value, receiver) {
method deleteProperty (line 886) | deleteProperty(target, key) {
method has (line 895) | has(target, key) {
method ownKeys (line 902) | ownKeys(target) {
class ReadonlyReactiveHandler (line 911) | class ReadonlyReactiveHandler extends BaseReactiveHandler {
method constructor (line 912) | constructor(isShallow2 = false) {
method set (line 915) | set(target, key) {
method deleteProperty (line 924) | deleteProperty(target, key) {
function get (line 943) | function get(target, key, isReadonly2 = false, isShallow2 = false) {
function has (line 963) | function has(key, isReadonly2 = false) {
function size (line 975) | function size(target, isReadonly2 = false) {
function add (line 980) | function add(value, _isShallow = false) {
function set (line 993) | function set(key, value, _isShallow = false) {
function deleteEntry (line 1015) | function deleteEntry(key) {
function clear (line 1032) | function clear() {
function createForEach (line 1042) | function createForEach(isReadonly2, isShallow2) {
function createIterableMethod (line 1054) | function createIterableMethod(method, isReadonly2, isShallow2) {
function createReadonlyMethod (line 1084) | function createReadonlyMethod(type) {
function createInstrumentations (line 1096) | function createInstrumentations() {
function createInstrumentationGetter (line 1190) | function createInstrumentationGetter(isReadonly2, shallow) {
function checkIdentityKeys (line 1219) | function checkIdentityKeys(target, has2, key) {
function targetTypeMap (line 1233) | function targetTypeMap(rawType) {
function getTargetType (line 1247) | function getTargetType(value) {
function reactive (line 1250) | function reactive(target) {
function shallowReactive (line 1262) | function shallowReactive(target) {
function readonly (line 1271) | function readonly(target) {
function shallowReadonly (line 1280) | function shallowReadonly(target) {
function createReactiveObject (line 1289) | function createReactiveObject(target, isReadonly2, baseHandlers, collect...
function isReactive (line 1318) | function isReactive(value) {
function isReadonly (line 1324) | function isReadonly(value) {
function isShallow (line 1327) | function isShallow(value) {
function isProxy (line 1330) | function isProxy(value) {
function toRaw (line 1333) | function toRaw(observed) {
function markRaw (line 1337) | function markRaw(value) {
constant COMPUTED_SIDE_EFFECT_WARN (line 1346) | const COMPUTED_SIDE_EFFECT_WARN = `Computed is still dirty after getter ...
class ComputedRefImpl (line 1347) | class ComputedRefImpl {
method constructor (line 1348) | constructor(getter, _setter, isReadonly, isSSR) {
method value (line 1365) | get value() {
method value (line 1381) | set value(newValue) {
method _dirty (line 1385) | get _dirty() {
method _dirty (line 1388) | set _dirty(v) {
function computed$1 (line 1393) | function computed$1(getterOrOptions, debugOptions, isSSR = false) {
function trackRefValue (line 1414) | function trackRefValue(ref2) {
function triggerRefValue (line 1432) | function triggerRefValue(ref2, dirtyLevel = 4, newVal, oldVal) {
function isRef (line 1449) | function isRef(r) {
function ref (line 1452) | function ref(value) {
function shallowRef (line 1455) | function shallowRef(value) {
function createRef (line 1458) | function createRef(rawValue, shallow) {
class RefImpl (line 1464) | class RefImpl {
method constructor (line 1465) | constructor(value, __v_isShallow) {
method value (line 1472) | get value() {
method value (line 1476) | set value(newVal) {
function triggerRef (line 1487) | function triggerRef(ref2) {
function unref (line 1490) | function unref(ref2) {
function toValue (line 1493) | function toValue(source) {
function proxyRefs (line 1508) | function proxyRefs(objectWithRefs) {
class CustomRefImpl (line 1511) | class CustomRefImpl {
method constructor (line 1512) | constructor(factory) {
method value (line 1522) | get value() {
method value (line 1525) | set value(newVal) {
function customRef (line 1529) | function customRef(factory) {
function toRefs (line 1532) | function toRefs(object) {
class ObjectRefImpl (line 1542) | class ObjectRefImpl {
method constructor (line 1543) | constructor(_object, _key, _defaultValue) {
method value (line 1549) | get value() {
method value (line 1553) | set value(newVal) {
method dep (line 1556) | get dep() {
class GetterRefImpl (line 1560) | class GetterRefImpl {
method constructor (line 1561) | constructor(_getter) {
method value (line 1566) | get value() {
function toRef (line 1570) | function toRef(source, key, defaultValue) {
function propertyToRef (line 1581) | function propertyToRef(source, key, defaultValue) {
function pushWarningContext (line 1599) | function pushWarningContext(vnode) {
function popWarningContext (line 1602) | function popWarningContext() {
function warn$1 (line 1606) | function warn$1(msg, ...args) {
function getComponentTrace (line 1643) | function getComponentTrace() {
function formatTrace (line 1664) | function formatTrace(trace) {
function formatTraceEntry (line 1672) | function formatTraceEntry({ vnode, recurseCount }) {
function formatProps (line 1683) | function formatProps(props) {
function formatProp (line 1694) | function formatProp(key, value, raw) {
function assertNumber (line 1710) | function assertNumber(val, type) {
function callWithErrorHandling (line 1786) | function callWithErrorHandling(fn, instance, type, args) {
function callWithAsyncErrorHandling (line 1793) | function callWithAsyncErrorHandling(fn, instance, type, args) {
function handleError (line 1815) | function handleError(err, instance, type, throwInDev = true) {
function logError (line 1847) | function logError(err, type, contextVNode, throwInDev = true) {
constant RECURSION_LIMIT (line 1874) | const RECURSION_LIMIT = 100;
function nextTick (line 1875) | function nextTick(fn) {
function findInsertionIndex (line 1879) | function findInsertionIndex(id) {
function queueJob (line 1894) | function queueJob(job) {
function queueFlush (line 1907) | function queueFlush() {
function invalidateJob (line 1913) | function invalidateJob(job) {
function queuePostFlushCb (line 1919) | function queuePostFlushCb(cb) {
function flushPreFlushCbs (line 1932) | function flushPreFlushCbs(instance, seen, i = isFlushing ? flushIndex + ...
function flushPostFlushCbs (line 1951) | function flushPostFlushCbs(seen) {
function flushJobs (line 1985) | function flushJobs(seen) {
function checkRecursiveUpdates (line 2018) | function checkRecursiveUpdates(seen, fn) {
function registerHMR (line 2048) | function registerHMR(instance) {
function unregisterHMR (line 2057) | function unregisterHMR(instance) {
function createRecord (line 2060) | function createRecord(id, initialDef) {
function normalizeClassComponent (line 2070) | function normalizeClassComponent(component) {
function rerender (line 2073) | function rerender(id, newRender) {
function reload (line 2091) | function reload(id, newComp) {
function updateComponentDef (line 2135) | function updateComponentDef(oldComp, newComp) {
function tryWrap (line 2143) | function tryWrap(fn) {
function emit$1 (line 2159) | function emit$1(event, ...args) {
function setDevtoolsHook$1 (line 2166) | function setDevtoolsHook$1(hook, target) {
function devtoolsInitApp (line 2198) | function devtoolsInitApp(app, version) {
function devtoolsUnmountApp (line 2206) | function devtoolsUnmountApp(app) {
function createDevtoolsComponentHook (line 2224) | function createDevtoolsComponentHook(hook) {
function createDevtoolsPerformanceHook (line 2241) | function createDevtoolsPerformanceHook(hook) {
function devtoolsComponentEmit (line 2246) | function devtoolsComponentEmit(component, event, params) {
function setCurrentRenderingInstance (line 2258) | function setCurrentRenderingInstance(instance) {
function pushScopeId (line 2264) | function pushScopeId(id) {
function popScopeId (line 2267) | function popScopeId() {
function withCtx (line 2271) | function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) {
function validateDirectiveName (line 2301) | function validateDirectiveName(name) {
function withDirectives (line 2306) | function withDirectives(vnode, directives) {
function invokeDirectiveHook (line 2337) | function invokeDirectiveHook(vnode, prevVNode, instance, name) {
function useTransitionState (line 2361) | function useTransitionState() {
method setup (line 2404) | setup(props, { slots }) {
function getLeavingNodesForType (line 2490) | function getLeavingNodesForType(state, vnode) {
function resolveTransitionHooks (line 2499) | function resolveTransitionHooks(vnode, props, state, instance, postClone) {
function emptyPlaceholder (line 2641) | function emptyPlaceholder(vnode) {
function getKeepAliveChild (line 2648) | function getKeepAliveChild(vnode) {
function setTransitionHooks (line 2665) | function setTransitionHooks(vnode, hooks) {
function getTransitionRawChildren (line 2675) | function getTransitionRawChildren(children, keepComment = false, parentK...
function defineComponent (line 2700) | function defineComponent(options, extraOptions) {
function defineAsyncComponent (line 2711) | function defineAsyncComponent(source) {
function createInnerComp (line 2838) | function createInnerComp(comp, parent) {
method setup (line 2859) | setup(props, { slots }) {
function matches (line 3041) | function matches(pattern, name) {
function onActivated (line 3051) | function onActivated(hook, target) {
function onDeactivated (line 3054) | function onDeactivated(hook, target) {
function registerKeepAliveHook (line 3057) | function registerKeepAliveHook(hook, type, target = currentInstance) {
function injectToKeepAliveRoot (line 3079) | function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) {
function resetShapeFlag (line 3091) | function resetShapeFlag(vnode) {
function getInnerChild (line 3095) | function getInnerChild(vnode) {
function injectHook (line 3099) | function injectHook(type, hook, target = currentInstance, prepend = fals...
function onErrorCaptured (line 3141) | function onErrorCaptured(hook, target = currentInstance) {
constant COMPONENTS (line 3145) | const COMPONENTS = "components";
constant DIRECTIVES (line 3146) | const DIRECTIVES = "directives";
function resolveComponent (line 3147) | function resolveComponent(name, maybeSelfReference) {
constant NULL_DYNAMIC_COMPONENT (line 3150) | const NULL_DYNAMIC_COMPONENT = Symbol.for("v-ndc");
function resolveDynamicComponent (line 3151) | function resolveDynamicComponent(component) {
function resolveDirective (line 3158) | function resolveDirective(name) {
function resolveAsset (line 3161) | function resolveAsset(type, name, warnMissing = true, maybeSelfReference...
function resolve (line 3195) | function resolve(registry, name) {
function renderList (line 3199) | function renderList(source, renderItem, cache, index) {
function createSlots (line 3238) | function createSlots(slots, dynamicSlots) {
function renderSlot (line 3256) | function renderSlot(slots, name, props = {}, fallback, noSlotted) {
function ensureValidVNode (line 3292) | function ensureValidVNode(vnodes) {
function toHandlers (line 3302) | function toHandlers(obj, preserveCaseIfNecessary) {
method get (line 3345) | get({ _: instance }, key) {
method set (line 3428) | set({ _: instance }, key, value) {
method has (line 3461) | has({
method defineProperty (line 3467) | defineProperty(target, key, descriptor) {
method get (line 3488) | get(target, key) {
method has (line 3494) | has(_, key) {
function createDevRenderContext (line 3507) | function createDevRenderContext(instance) {
function exposePropsOnRenderContext (line 3526) | function exposePropsOnRenderContext(instance) {
function exposeSetupStateOnRenderContext (line 3542) | function exposeSetupStateOnRenderContext(instance) {
function defineProps (line 3567) | function defineProps() {
function defineEmits (line 3573) | function defineEmits() {
function defineExpose (line 3579) | function defineExpose(exposed) {
function defineOptions (line 3584) | function defineOptions(options) {
function defineSlots (line 3589) | function defineSlots() {
function defineModel (line 3595) | function defineModel() {
function withDefaults (line 3600) | function withDefaults(props, defaults) {
function useSlots (line 3606) | function useSlots() {
function useAttrs (line 3609) | function useAttrs() {
function getContext (line 3612) | function getContext() {
function normalizePropsOrEmits (line 3619) | function normalizePropsOrEmits(props) {
function mergeDefaults (line 3625) | function mergeDefaults(raw, defaults) {
function mergeModels (line 3647) | function mergeModels(a, b) {
function createPropsRestProxy (line 3652) | function createPropsRestProxy(props, excludedKeys) {
function withAsyncContext (line 3664) | function withAsyncContext(getAwaitable) {
function createDuplicateChecker (line 3682) | function createDuplicateChecker() {
function applyOptions (line 3693) | function applyOptions(instance) {
function resolveInjections (line 3882) | function resolveInjections(injectOptions, ctx, checkDuplicateProperties ...
function callHook$1 (line 3917) | function callHook$1(hook, instance, type) {
function createWatcher (line 3924) | function createWatcher(raw, ctx, publicThis, key) {
function resolveMergedOptions (line 3950) | function resolveMergedOptions(instance) {
function mergeOptions (line 3980) | function mergeOptions(to, from, strats, asMixin = false) {
function mergeDataFn (line 4033) | function mergeDataFn(to, from) {
function mergeInject (line 4047) | function mergeInject(to, from) {
function normalizeInject (line 4050) | function normalizeInject(raw) {
function mergeAsArray$1 (line 4060) | function mergeAsArray$1(to, from) {
function mergeObjectOptions (line 4063) | function mergeObjectOptions(to, from) {
function mergeEmitsOrPropsOptions (line 4066) | function mergeEmitsOrPropsOptions(to, from) {
function mergeWatchOptions (line 4080) | function mergeWatchOptions(to, from) {
function createAppContext (line 4090) | function createAppContext() {
function createAppAPI (line 4112) | function createAppAPI(render, hydrate) {
function provide (line 4276) | function provide(key, value) {
function inject (line 4290) | function inject(key, defaultValue, treatDefaultAsFactory = false) {
function hasInjectionContext (line 4305) | function hasInjectionContext() {
function initProps (line 4313) | function initProps(instance, rawProps, isStateful, isSSR = false) {
function isInHmrContext (line 4337) | function isInHmrContext(instance) {
function updateProps (line 4343) | function updateProps(instance, rawProps, rawPrevProps, optimized) {
function setFullProps (line 4435) | function setFullProps(instance, rawProps, props, attrs) {
function resolvePropValue (line 4477) | function resolvePropValue(options, props, key, value, instance, isAbsent) {
function normalizePropsOptions (line 4510) | function normalizePropsOptions(comp, appContext, asMixin = false) {
function validatePropName (line 4593) | function validatePropName(key) {
function getType (line 4601) | function getType(ctor) {
function validateProps (line 4613) | function validateProps(rawProps, props, instance) {
function validateProp (line 4628) | function validateProp(name, value, prop, props, isAbsent) {
function assertType (line 4658) | function assertType(value, type) {
function getInvalidTypeMessage (line 4681) | function getInvalidTypeMessage(name, value, expectedTypes) {
function styleValue (line 4699) | function styleValue(value, type) {
function isExplicable (line 4708) | function isExplicable(type) {
function isBoolean (line 4712) | function isBoolean(...args) {
function setRef (line 4816) | function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = fa...
method process (line 4936) | process(n1, n2, container, anchor, parentComponent, parentSuspense, name...
method remove (line 5070) | remove(vnode, parentComponent, parentSuspense, { um: unmount, o: { remov...
function moveTeleport (line 5102) | function moveTeleport(vnode, container, parentAnchor, { o: { insert }, m...
function hydrateTeleport (line 5127) | function hydrateTeleport(node, vnode, parentComponent, parentSuspense, s...
function updateCssVars (line 5183) | function updateCssVars(vnode) {
function prepareAnchor (line 5194) | function prepareAnchor(target, vnode, createText, insert) {
function createHydrationFunctions (line 5221) | function createHydrationFunctions(rendererInternals) {
function propHasMismatch (line 5670) | function propHasMismatch(el, key, clientValue, vnode, instance) {
function toClassSet (line 5736) | function toClassSet(str) {
function isSetEqual (line 5739) | function isSetEqual(a, b) {
function toStyleMap (line 5750) | function toStyleMap(str) {
function isMapEqual (line 5762) | function isMapEqual(a, b) {
function resolveCssVars (line 5773) | function resolveCssVars(instance, vnode, expectedMap) {
function startMeasure (line 5788) | function startMeasure(instance, type) {
function endMeasure (line 5796) | function endMeasure(instance, type) {
function isSupported (line 5813) | function isSupported() {
function createRenderer (line 5827) | function createRenderer(options) {
function createHydrationRenderer (line 5830) | function createHydrationRenderer(options) {
function baseCreateRenderer (line 5833) | function baseCreateRenderer(options, createHydrationFns) {
function resolveChildrenNamespace (line 7225) | function resolveChildrenNamespace({ type, props }, currentNamespace) {
function toggleRecurse (line 7228) | function toggleRecurse({ effect, update }, allowed) {
function needTransition (line 7231) | function needTransition(parentSuspense, transition) {
function traverseStaticChildren (line 7234) | function traverseStaticChildren(n1, n2, shallow = false) {
function getSequence (line 7258) | function getSequence(arr) {
function locateNonHydratedAsyncRoot (line 7298) | function locateNonHydratedAsyncRoot(instance) {
function invalidateMount (line 7308) | function invalidateMount(hooks) {
function watchEffect (line 7327) | function watchEffect(effect, options) {
function watchPostEffect (line 7330) | function watchPostEffect(effect, options) {
function watchSyncEffect (line 7337) | function watchSyncEffect(effect, options) {
constant INITIAL_WATCHER_VALUE (line 7344) | const INITIAL_WATCHER_VALUE = {};
function watch (line 7345) | function watch(source, cb, options) {
function doWatch (line 7353) | function doWatch(source, cb, {
function instanceWatch (line 7518) | function instanceWatch(source, value, options) {
function createPathGetter (line 7533) | function createPathGetter(ctx, path) {
function traverse (line 7543) | function traverse(value, depth = Infinity, seen) {
function useModel (line 7576) | function useModel(props, name, options = EMPTY_OBJ) {
function emit (line 7643) | function emit(instance, event, ...rawArgs) {
function normalizeEmitsOptions (line 7728) | function normalizeEmitsOptions(comp, appContext, asMixin = false) {
function isEmitListener (line 7771) | function isEmitListener(options, key) {
function markAttrsAccessed (line 7780) | function markAttrsAccessed() {
function renderComponentRoot (line 7783) | function renderComponentRoot(instance) {
function filterSingleRoot (line 7952) | function filterSingleRoot(children, recurse = true) {
function shouldUpdateComponent (line 7994) | function shouldUpdateComponent(prevVNode, nextVNode, optimized) {
function hasPropsChanged (line 8041) | function hasPropsChanged(prevProps, nextProps, emitsOptions) {
function updateHOCHostEl (line 8054) | function updateHOCHostEl({ vnode, parent }, el) {
method process (line 8078) | process(n1, n2, container, anchor, parentComponent, parentSuspense, name...
function triggerEvent (line 8115) | function triggerEvent(vnode, name) {
function mountSuspense (line 8121) | function mountSuspense(vnode, container, anchor, parentComponent, parent...
function patchSuspense (line 8168) | function patchSuspense(n1, n2, container, anchor, parentComponent, names...
function createSuspenseBoundary (line 8329) | function createSuspenseBoundary(vnode, parentSuspense, parentComponent, ...
function hydrateSuspense (line 8568) | function hydrateSuspense(node, vnode, parentComponent, parentSuspense, n...
function normalizeSuspenseChildren (line 8596) | function normalizeSuspenseChildren(vnode) {
function normalizeSuspenseSlot (line 8604) | function normalizeSuspenseSlot(s) {
function queueEffectWithSuspense (line 8632) | function queueEffectWithSuspense(fn, suspense) {
function setActiveBranch (line 8643) | function setActiveBranch(suspense, branch) {
function isVNodeSuspensible (line 8657) | function isVNodeSuspensible(vnode) {
function openBlock (line 8668) | function openBlock(disableTracking = false) {
function closeBlock (line 8671) | function closeBlock() {
function setBlockTracking (line 8676) | function setBlockTracking(value) {
function setupBlock (line 8682) | function setupBlock(vnode) {
function createElementBlock (line 8690) | function createElementBlock(type, props, children, patchFlag, dynamicPro...
function createBlock (line 8703) | function createBlock(type, props, children, patchFlag, dynamicProps) {
function isVNode (line 8715) | function isVNode(value) {
function isSameVNodeType (line 8718) | function isSameVNodeType(n1, n2) {
function transformVNodeArgs (line 8730) | function transformVNodeArgs(transformer) {
function createBaseVNode (line 8749) | function createBaseVNode(type, props = null, children = null, patchFlag ...
function _createVNode (line 8804) | function _createVNode(type, props = null, children = null, patchFlag = 0...
function guardReactiveProps (line 8868) | function guardReactiveProps(props) {
function cloneVNode (line 8872) | function cloneVNode(vnode, extraProps, mergeRef = false, cloneTransition...
function deepCloneVNode (line 8926) | function deepCloneVNode(vnode) {
function createTextVNode (line 8933) | function createTextVNode(text = " ", flag = 0) {
function createStaticVNode (line 8936) | function createStaticVNode(content, numberOfNodes) {
function createCommentVNode (line 8941) | function createCommentVNode(text = "", asBlock = false) {
function normalizeVNode (line 8944) | function normalizeVNode(child) {
function cloneIfMounted (line 8960) | function cloneIfMounted(child) {
function normalizeChildren (line 8963) | function normalizeChildren(vnode, children) {
function mergeProps (line 9008) | function mergeProps(...args) {
function invokeVNodeHook (line 9032) | function invokeVNodeHook(hook, instance, vnode, prevVNode = null) {
function createComponentInstance (line 9041) | function createComponentInstance(vnode, parent, suspense) {
function validateComponentName (line 9154) | function validateComponentName(name, { isNativeTag }) {
function isStatefulComponent (line 9161) | function isStatefulComponent(instance) {
function setupComponent (line 9165) | function setupComponent(instance, isSSR = false, optimized = false) {
function setupStatefulComponent (line 9175) | function setupStatefulComponent(instance, isSSR) {
function handleSetupResult (line 9245) | function handleSetupResult(instance, setupResult, isSSR) {
function registerRuntimeCompiler (line 9272) | function registerRuntimeCompiler(_compile) {
function finishComponentSetup (line 9281) | function finishComponentSetup(instance, isSSR, skipOptions) {
method get (line 9334) | get(target, key) {
method set (line 9339) | set() {
method deleteProperty (line 9343) | deleteProperty() {
function getSlotsProxy (line 9348) | function getSlotsProxy(instance) {
function createSetupContext (line 9356) | function createSetupContext(instance) {
function getComponentPublicInstance (line 9397) | function getComponentPublicInstance(instance) {
function getComponentName (line 9417) | function getComponentName(Component, includeInferred = true) {
function formatComponentName (line 9420) | function formatComponentName(instance, Component, isRoot = false) {
function isClassComponent (line 9442) | function isClassComponent(value) {
function h (line 9457) | function h(type, propsOrChildren, children) {
function initCustomFormatter (line 9478) | function initCustomFormatter() {
function withMemo (line 9655) | function withMemo(memo, render, cache, index) {
function isMemoSame (line 9665) | function isMemoSame(cached, memo) {
method setScopeId (line 9723) | setScopeId(el, id) {
method insertStaticContent (line 9730) | insertStaticContent(content, parent, anchor, namespace, start, end) {
constant ANIMATION (line 9759) | const ANIMATION = "animation";
function resolveTransitionProps (line 9796) | function resolveTransitionProps(rawProps) {
function normalizeDuration (line 9904) | function normalizeDuration(duration) {
function NumberOf (line 9914) | function NumberOf(val) {
function addTransitionClass (line 9921) | function addTransitionClass(el, cls) {
function removeTransitionClass (line 9925) | function removeTransitionClass(el, cls) {
function nextFrame (line 9935) | function nextFrame(cb) {
function whenTransitionEnds (line 9941) | function whenTransitionEnds(el, expectedType, explicitTimeout, resolve) {
function getTransitionInfo (line 9973) | function getTransitionInfo(el, expectedType) {
function getTimeout (line 10012) | function getTimeout(delays, durations) {
function toMs (line 10018) | function toMs(s) {
function forceReflow (line 10022) | function forceReflow() {
function patchClass (line 10026) | function patchClass(el, value, isSVG) {
method beforeMount (line 10043) | beforeMount(el, { value }, { transition }) {
method mounted (line 10051) | mounted(el, { value }, { transition }) {
method updated (line 10056) | updated(el, { value, oldValue }, { transition }) {
method beforeUnmount (line 10072) | beforeUnmount(el, { value }) {
function setDisplay (line 10079) | function setDisplay(el, value) {
constant CSS_VAR_TEXT (line 10084) | const CSS_VAR_TEXT = Symbol("CSS_VAR_TEXT" );
function useCssVars (line 10085) | function useCssVars(getter) {
function setVarsOnVNode (line 10111) | function setVarsOnVNode(vnode, vars) {
function setVarsOnNode (line 10137) | function setVarsOnNode(el, vars) {
function patchStyle (line 10150) | function patchStyle(el, prev, next) {
function setStyle (line 10200) | function setStyle(style, name, val) {
function autoPrefix (line 10230) | function autoPrefix(style, rawName) {
function patchAttr (line 10250) | function patchAttr(el, key, value, isSVG, instance, isBoolean = isSpecia...
function patchDOMProp (line 10269) | function patchDOMProp(el, key, value, parentComponent) {
function addEventListener (line 10315) | function addEventListener(el, event, handler, options) {
function removeEventListener (line 10318) | function removeEventListener(el, event, handler, options) {
function patchEvent (line 10322) | function patchEvent(el, rawName, prevValue, nextValue, instance = null) {
function parseName (line 10342) | function parseName(name) {
function createInvoker (line 10358) | function createInvoker(initialValue, instance) {
function sanitizeEventValue (line 10376) | function sanitizeEventValue(value, propName) {
function patchStopImmediatePropagation (line 10386) | function patchStopImmediatePropagation(e, value) {
function shouldSetAsProp (line 10427) | function shouldSetAsProp(el, key, value, isSVG) {
function defineCustomElement (line 10463) | function defineCustomElement(options, extraOptions, hydrate2) {
class VueElement (line 10479) | class VueElement extends BaseClass {
method constructor (line 10480) | constructor(_def, _props = {}, hydrate2) {
method connectedCallback (line 10506) | connectedCallback() {
method disconnectedCallback (line 10516) | disconnectedCallback() {
method _resolveDef (line 10532) | _resolveDef() {
method _resolveProps (line 10571) | _resolveProps(def) {
method _setAttr (line 10590) | _setAttr(key) {
method _getProp (line 10601) | _getProp(key) {
method _setProp (line 10607) | _setProp(key, val, shouldReflect = true, shouldUpdate = true) {
method _update (line 10624) | _update() {
method _createVNode (line 10627) | _createVNode() {
method _applyStyles (line 10669) | _applyStyles(styles) {
function useCssModule (line 10683) | function useCssModule(name = "$style") {
method setup (line 10714) | setup(props, { slots }) {
function callPendingCbs (line 10798) | function callPendingCbs(c) {
function recordPosition (line 10807) | function recordPosition(c) {
function applyTranslation (line 10810) | function applyTranslation(c) {
function hasCSSTransform (line 10822) | function hasCSSTransform(el, root, moveClass) {
function onCompositionStart (line 10843) | function onCompositionStart(e) {
function onCompositionEnd (line 10846) | function onCompositionEnd(e) {
method created (line 10855) | created(el, { modifiers: { lazy, trim, number } }, vnode) {
method mounted (line 10881) | mounted(el, { value }) {
method beforeUpdate (line 10884) | beforeUpdate(el, { value, oldValue, modifiers: { lazy, trim, number } },...
method created (line 10906) | created(el, _, vnode) {
method beforeUpdate (line 10938) | beforeUpdate(el, binding, vnode) {
function setChecked (line 10943) | function setChecked(el, { value, oldValue }, vnode) {
method created (line 10954) | created(el, { value }, vnode) {
method beforeUpdate (line 10961) | beforeUpdate(el, { value, oldValue }, vnode) {
method created (line 10971) | created(el, { value, modifiers: { number } }, vnode) {
method mounted (line 10989) | mounted(el, { value, modifiers: { number } }) {
method beforeUpdate (line 10992) | beforeUpdate(el, _binding, vnode) {
method updated (line 10995) | updated(el, { value, modifiers: { number } }) {
function setSelected (line 11001) | function setSelected(el, value, number) {
function getValue (line 11033) | function getValue(el) {
function getCheckboxValue (line 11036) | function getCheckboxValue(el, checked) {
method created (line 11041) | created(el, binding, vnode) {
method mounted (line 11044) | mounted(el, binding, vnode) {
method beforeUpdate (line 11047) | beforeUpdate(el, binding, vnode, prevVNode) {
method updated (line 11050) | updated(el, binding, vnode, prevVNode) {
function resolveDynamicModel (line 11054) | function resolveDynamicModel(tagName, type) {
function callModelHook (line 11071) | function callModelHook(el, binding, vnode, prevVNode, hook) {
function ensureRenderer (line 11131) | function ensureRenderer() {
function ensureHydrationRenderer (line 11134) | function ensureHydrationRenderer() {
function resolveRootNamespace (line 11184) | function resolveRootNamespace(container) {
function injectNativeTagCheck (line 11192) | function injectNativeTagCheck(app) {
function injectCompilerOptionsCheck (line 11198) | function injectCompilerOptionsCheck(app) {
function normalizeContainer (line 11227) | function normalizeContainer(container) {
function initDev (line 11408) | function initDev() {
constant FRAGMENT (line 11420) | const FRAGMENT = Symbol(`Fragment` );
constant TELEPORT (line 11421) | const TELEPORT = Symbol(`Teleport` );
constant SUSPENSE (line 11422) | const SUSPENSE = Symbol(`Suspense` );
constant KEEP_ALIVE (line 11423) | const KEEP_ALIVE = Symbol(`KeepAlive` );
constant BASE_TRANSITION (line 11424) | const BASE_TRANSITION = Symbol(`BaseTransition` );
constant OPEN_BLOCK (line 11425) | const OPEN_BLOCK = Symbol(`openBlock` );
constant CREATE_BLOCK (line 11426) | const CREATE_BLOCK = Symbol(`createBlock` );
constant CREATE_ELEMENT_BLOCK (line 11427) | const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock` );
constant CREATE_VNODE (line 11428) | const CREATE_VNODE = Symbol(`createVNode` );
constant CREATE_ELEMENT_VNODE (line 11429) | const CREATE_ELEMENT_VNODE = Symbol(`createElementVNode` );
constant CREATE_COMMENT (line 11430) | const CREATE_COMMENT = Symbol(`createCommentVNode` );
constant CREATE_TEXT (line 11431) | const CREATE_TEXT = Symbol(`createTextVNode` );
constant CREATE_STATIC (line 11432) | const CREATE_STATIC = Symbol(`createStaticVNode` );
constant RESOLVE_COMPONENT (line 11433) | const RESOLVE_COMPONENT = Symbol(`resolveComponent` );
constant RESOLVE_DYNAMIC_COMPONENT (line 11434) | const RESOLVE_DYNAMIC_COMPONENT = Symbol(
constant RESOLVE_DIRECTIVE (line 11437) | const RESOLVE_DIRECTIVE = Symbol(`resolveDirective` );
constant RESOLVE_FILTER (line 11438) | const RESOLVE_FILTER = Symbol(`resolveFilter` );
constant WITH_DIRECTIVES (line 11439) | const WITH_DIRECTIVES = Symbol(`withDirectives` );
constant RENDER_LIST (line 11440) | const RENDER_LIST = Symbol(`renderList` );
constant RENDER_SLOT (line 11441) | const RENDER_SLOT = Symbol(`renderSlot` );
constant CREATE_SLOTS (line 11442) | const CREATE_SLOTS = Symbol(`createSlots` );
constant TO_DISPLAY_STRING (line 11443) | const TO_DISPLAY_STRING = Symbol(`toDisplayString` );
constant MERGE_PROPS (line 11444) | const MERGE_PROPS = Symbol(`mergeProps` );
constant NORMALIZE_CLASS (line 11445) | const NORMALIZE_CLASS = Symbol(`normalizeClass` );
constant NORMALIZE_STYLE (line 11446) | const NORMALIZE_STYLE = Symbol(`normalizeStyle` );
constant NORMALIZE_PROPS (line 11447) | const NORMALIZE_PROPS = Symbol(`normalizeProps` );
constant GUARD_REACTIVE_PROPS (line 11448) | const GUARD_REACTIVE_PROPS = Symbol(`guardReactiveProps` );
constant TO_HANDLERS (line 11449) | const TO_HANDLERS = Symbol(`toHandlers` );
constant CAMELIZE (line 11450) | const CAMELIZE = Symbol(`camelize` );
constant CAPITALIZE (line 11451) | const CAPITALIZE = Symbol(`capitalize` );
constant TO_HANDLER_KEY (line 11452) | const TO_HANDLER_KEY = Symbol(`toHandlerKey` );
constant SET_BLOCK_TRACKING (line 11453) | const SET_BLOCK_TRACKING = Symbol(`setBlockTracking` );
constant PUSH_SCOPE_ID (line 11454) | const PUSH_SCOPE_ID = Symbol(`pushScopeId` );
constant POP_SCOPE_ID (line 11455) | const POP_SCOPE_ID = Symbol(`popScopeId` );
constant WITH_CTX (line 11456) | const WITH_CTX = Symbol(`withCtx` );
constant UNREF (line 11457) | const UNREF = Symbol(`unref` );
constant IS_REF (line 11458) | const IS_REF = Symbol(`isRef` );
constant WITH_MEMO (line 11459) | const WITH_MEMO = Symbol(`withMemo` );
constant IS_MEMO_SAME (line 11460) | const IS_MEMO_SAME = Symbol(`isMemoSame` );
function registerRuntimeHelpers (line 11502) | function registerRuntimeHelpers(helpers) {
function createRoot (line 11513) | function createRoot(children, source = "") {
function createVNodeCall (line 11529) | function createVNodeCall(context, tag, props, children, patchFlag, dynam...
function createArrayExpression (line 11555) | function createArrayExpression(elements, loc = locStub) {
function createObjectExpression (line 11562) | function createObjectExpression(properties, loc = locStub) {
function createObjectProperty (line 11569) | function createObjectProperty(key, value) {
function createSimpleExpression (line 11577) | function createSimpleExpression(content, isStatic = false, loc = locStub...
function createCompoundExpression (line 11586) | function createCompoundExpression(children, loc = locStub) {
function createCallExpression (line 11593) | function createCallExpression(callee, args = [], loc = locStub) {
function createFunctionExpression (line 11601) | function createFunctionExpression(params, returns = void 0, newline = fa...
function createConditionalExpression (line 11611) | function createConditionalExpression(test, consequent, alternate, newlin...
function createCacheExpression (line 11621) | function createCacheExpression(index, value, isVOnce = false) {
function createBlockStatement (line 11630) | function createBlockStatement(body) {
function getVNodeHelper (line 11637) | function getVNodeHelper(ssr, isComponent) {
function getVNodeBlockHelper (line 11640) | function getVNodeBlockHelper(ssr, isComponent) {
function convertToBlock (line 11643) | function convertToBlock(node, { helper, removeHelper, inSSR }) {
function isTagStartChar (line 11654) | function isTagStartChar(c) {
function isWhitespace (line 11657) | function isWhitespace(c) {
function isEndOfTagSection (line 11660) | function isEndOfTagSection(c) {
function toCharCodes (line 11663) | function toCharCodes(str) {
class Tokenizer (line 11697) | class Tokenizer {
method constructor (line 11698) | constructor(stack, cbs) {
method inSFCRoot (line 11728) | get inSFCRoot() {
method reset (line 11731) | reset() {
method getPos (line 11750) | getPos(index) {
method peek (line 11767) | peek() {
method stateText (line 11770) | stateText(c) {
method stateInterpolationOpen (line 11783) | stateInterpolationOpen(c) {
method stateInterpolation (line 11803) | stateInterpolation(c) {
method stateInterpolationClose (line 11810) | stateInterpolationClose(c) {
method stateSpecialStartSequence (line 11828) | stateSpecialStartSequence(c) {
method stateInRCDATA (line 11848) | stateInRCDATA(c) {
method stateCDATASequence (line 11881) | stateCDATASequence(c) {
method fastForwardTo (line 11901) | fastForwardTo(c) {
method stateInCommentLike (line 11922) | stateInCommentLike(c) {
method startSpecial (line 11942) | startSpecial(sequence, offset) {
method enterRCDATA (line 11946) | enterRCDATA(sequence, offset) {
method stateBeforeTagName (line 11951) | stateBeforeTagName(c) {
method stateInTagName (line 11980) | stateInTagName(c) {
method stateInSFCRootTagName (line 11985) | stateInSFCRootTagName(c) {
method handleTagName (line 11994) | handleTagName(c) {
method stateBeforeClosingTagName (line 12000) | stateBeforeClosingTagName(c) {
method stateInClosingTagName (line 12012) | stateInClosingTagName(c) {
method stateAfterClosingTagName (line 12020) | stateAfterClosingTagName(c) {
method stateBeforeAttrName (line 12026) | stateBeforeAttrName(c) {
method handleAttrStart (line 12054) | handleAttrStart(c) {
method stateInSelfClosingTag (line 12067) | stateInSelfClosingTag(c) {
method stateInAttrName (line 12078) | stateInAttrName(c) {
method stateInDirName (line 12089) | stateInDirName(c) {
method stateInDirArg (line 12103) | stateInDirArg(c) {
method stateInDynamicDirArg (line 12115) | stateInDynamicDirArg(c) {
method stateInDirModifier (line 12129) | stateInDirModifier(c) {
method handleAttrNameEnd (line 12138) | handleAttrNameEnd(c) {
method stateAfterAttrName (line 12144) | stateAfterAttrName(c) {
method stateBeforeAttrValue (line 12157) | stateBeforeAttrValue(c) {
method handleInAttrValue (line 12170) | handleInAttrValue(c, quote) {
method stateInAttrValueDoubleQuotes (line 12181) | stateInAttrValueDoubleQuotes(c) {
method stateInAttrValueSingleQuotes (line 12184) | stateInAttrValueSingleQuotes(c) {
method stateInAttrValueNoQuotes (line 12187) | stateInAttrValueNoQuotes(c) {
method stateBeforeDeclaration (line 12201) | stateBeforeDeclaration(c) {
method stateInDeclaration (line 12209) | stateInDeclaration(c) {
method stateInProcessingInstruction (line 12215) | stateInProcessingInstruction(c) {
method stateBeforeComment (line 12222) | stateBeforeComment(c) {
method stateInSpecialComment (line 12232) | stateInSpecialComment(c) {
method stateBeforeSpecialS (line 12239) | stateBeforeSpecialS(c) {
method stateBeforeSpecialT (line 12249) | stateBeforeSpecialT(c) {
method startEntity (line 12259) | startEntity() {
method stateInEntity (line 12261) | stateInEntity() {
method parse (line 12268) | parse(input) {
method cleanup (line 12421) | cleanup() {
method finish (line 12432) | finish() {
method handleTrailingData (line 12437) | handleTrailingData() {
method emitCodePoint (line 12452) | emitCodePoint(cp, consumed) {
function defaultOnError (line 12456) | function defaultOnError(error) {
function defaultOnWarn (line 12459) | function defaultOnWarn(msg) {
function createCompilerError (line 12462) | function createCompilerError(code, loc, messages, additionalMessage) {
function isCoreComponent (line 12533) | function isCoreComponent(tag) {
function assert (line 12617) | function assert(condition, msg) {
function findDir (line 12622) | function findDir(node, name, allowEmpty = false) {
function findProp (line 12630) | function findProp(node, name, dynamicOnly = false, allowEmpty = false) {
function isStaticArgOf (line 12643) | function isStaticArgOf(arg, name) {
function hasDynamicKeyVBind (line 12646) | function hasDynamicKeyVBind(node) {
function isText$1 (line 12654) | function isText$1(node) {
function isVSlot (line 12657) | function isVSlot(p) {
function isTemplateNode (line 12660) | function isTemplateNode(node) {
function isSlotOutlet (line 12663) | function isSlotOutlet(node) {
function getUnnormalizedProps (line 12667) | function getUnnormalizedProps(props, callPath = []) {
function injectProp (line 12679) | function injectProp(node, prop, context) {
function hasProp (line 12737) | function hasProp(prop, props) {
function toValidAssetId (line 12747) | function toValidAssetId(name, type) {
function getMemoedVNodeCall (line 12752) | function getMemoedVNodeCall(node) {
method ontext (line 12788) | ontext(start, end) {
method ontextentity (line 12791) | ontextentity(char, start, end) {
method oninterpolation (line 12794) | oninterpolation(start, end) {
method onopentagname (line 12818) | onopentagname(start, end) {
method onopentagend (line 12832) | onopentagend(end) {
method onclosetag (line 12835) | onclosetag(start, end) {
method onselfclosingtag (line 12858) | onselfclosingtag(end) {
method onattribname (line 12866) | onattribname(start, end) {
method ondirname (line 12875) | ondirname(start, end) {
method ondirarg (line 12911) | ondirarg(start, end) {
method ondirmodifier (line 12927) | ondirmodifier(start, end) {
method onattribdata (line 12942) | onattribdata(start, end) {
method onattribentity (line 12947) | onattribentity(char, start, end) {
method onattribnameend (line 12952) | onattribnameend(end) {
method onattribend (line 12964) | onattribend(quote, end) {
method oncomment (line 13010) | oncomment(start, end) {
method onend (line 13019) | onend() {
method oncdata (line 13064) | oncdata(start, end) {
method onprocessinginstruction (line 13071) | onprocessinginstruction(start) {
function parseForExpression (line 13082) | function parseForExpression(input) {
function getSlice (line 13136) | function getSlice(start, end) {
function endOpenTag (line 13139) | function endOpenTag(end) {
function onText (line 13158) | function onText(content, start, end) {
function onCloseTag (line 13178) | function onCloseTag(el, end, isImplied = false) {
function lookAhead (line 13219) | function lookAhead(index, c) {
function backTrack (line 13224) | function backTrack(index, c) {
function isFragmentTemplate (line 13230) | function isFragmentTemplate({ tag, props }) {
function isComponent (line 13240) | function isComponent({ tag, props }) {
function isUpperCase (line 13259) | function isUpperCase(c) {
function condenseWhitespace (line 13263) | function condenseWhitespace(nodes, tag) {
function isAllWhitespace (line 13295) | function isAllWhitespace(str) {
function hasNewlineChar (line 13303) | function hasNewlineChar(str) {
function condense (line 13312) | function condense(str) {
function addNode (line 13328) | function addNode(node) {
function getLoc (line 13331) | function getLoc(start, end) {
function setLocEnd (line 13340) | function setLocEnd(loc, end) {
function dirToAttr (line 13344) | function dirToAttr(dir) {
function createExp (line 13371) | function createExp(content, isStatic = false, loc, constType = 0, parseM...
function emitError (line 13375) | function emitError(code, index, message) {
function reset (line 13380) | function reset() {
function baseParse (line 13389) | function baseParse(input, options) {
function hoistStatic (line 13423) | function hoistStatic(root, context) {
function isSingleElementRoot (line 13432) | function isSingleElementRoot(root, child) {
function walk (line 13436) | function walk(node, context, doNotHoistNode = false) {
function getConstantType (line 13501) | function getConstantType(node, context) {
function getConstantTypeOfHelperCall (line 13612) | function getConstantTypeOfHelperCall(value, context) {
function getGeneratedPropsConstantType (line 13623) | function getGeneratedPropsConstantType(node, context) {
function getNodeProps (line 13655) | function getNodeProps(node) {
function createTransformContext (line 13662) | function createTransformContext(root, {
function transform (line 13808) | function transform(root, options) {
function createRootCodegen (line 13826) | function createRootCodegen(root, context) {
function traverseChildren (line 13861) | function traverseChildren(parent, context) {
function traverseNode (line 13876) | function traverseNode(node, context) {
function createStructuralDirectiveTransform (line 13924) | function createStructuralDirectiveTransform(name, fn) {
constant PURE_ANNOTATION (line 13947) | const PURE_ANNOTATION = `/*#__PURE__*/`;
function createCodegenContext (line 13949) | function createCodegenContext(ast, {
function generate (line 14009) | function generate(ast, options = {}) {
function genFunctionPreamble (line 14092) | function genFunctionPreamble(ast, context) {
function genAssets (line 14125) | function genAssets(assets, type, { helper, push, newline, isTS }) {
function genHoists (line 14143) | function genHoists(hoists, context) {
function isText (line 14162) | function isText(n) {
function genNodeListAsArray (line 14165) | function genNodeListAsArray(nodes, context) {
function genNodeList (line 14173) | function genNodeList(nodes, context, multilines = false, comma = true) {
function genNode (line 14194) | function genNode(node, context) {
function genText (line 14275) | function genText(node, context) {
function genExpression (line 14278) | function genExpression(node, context) {
function genInterpolation (line 14286) | function genInterpolation(node, context) {
function genCompoundExpression (line 14293) | function genCompoundExpression(node, context) {
function genExpressionAsPropertyKey (line 14303) | function genExpressionAsPropertyKey(node, context) {
function genComment (line 14316) | function genComment(node, context) {
function genVNodeCall (line 14327) | function genVNodeCall(node, context) {
function genNullableArgs (line 14376) | function genNullableArgs(args) {
function genCallExpression (line 14383) | function genCallExpression(node, context) {
function genObjectExpression (line 14393) | function genObjectExpression(node, context) {
function genArrayExpression (line 14416) | function genArrayExpression(node, context) {
function genFunctionExpression (line 14419) | function genFunctionExpression(node, context) {
function genConditionalExpression (line 14456) | function genConditionalExpression(node, context) {
function genCacheExpression (line 14491) | function genCacheExpression(node, context) {
function validateBrowserExpression (line 14517) | function validateBrowserExpression(node, context, asParams = false, asRa...
function processExpression (line 14570) | function processExpression(node, context, asParams = false, asRawStateme...
function processIf (line 14611) | function processIf(node, dir, context, processCodegen) {
function createIfBranch (line 14689) | function createIfBranch(node, dir) {
function createCodegenNodeForBranch (line 14700) | function createCodegenNodeForBranch(branch, keyIndex, context) {
function createChildrenCodegenNode (line 14716) | function createChildrenCodegenNode(branch, keyIndex, context) {
function isSameKey (line 14766) | function isSameKey(a, b) {
function getParentCondition (line 14786) | function getParentCondition(node) {
function processFor (line 15006) | function processFor(node, dir, context, processCodegen) {
function finalizeForParseResult (line 15041) | function finalizeForParseResult(result, context) {
function createForLoopParams (line 15069) | function createForLoopParams({ value, key, index }, memoArgs = []) {
function createParamsList (line 15072) | function createParamsList(args) {
function buildSlots (line 15100) | function buildSlots(node, context, buildSlotFn = buildClientSlotFn) {
function buildDynamicSlot (line 15291) | function buildDynamicSlot(name, fn, index) {
function hasForwardedSlots (line 15303) | function hasForwardedSlots(children) {
function isNonWhitespaceContent (line 15323) | function isNonWhitespaceContent(node) {
function resolveComponentType (line 15430) | function resolveComponentType(node, context, ssr = false) {
function buildProps (line 15469) | function buildProps(node, context, props = node.props, isComponent, isDy...
function dedupeProperties (line 15747) | function dedupeProperties(properties) {
function mergeAsArray (line 15769) | function mergeAsArray(existing, incoming) {
function buildDirectiveArgs (line 15779) | function buildDirectiveArgs(dir, context) {
function stringifyDynamicPropNames (line 15818) | function stringifyDynamicPropNames(props) {
function isComponentTag (line 15826) | function isComponentTag(tag) {
function processSlotOutlet (line 15861) | function processSlotOutlet(node, context) {
function createTransformProps (line 16149) | function createTransformProps(props = []) {
function getBaseTransformPreset (line 16178) | function getBaseTransformPreset(prefixIdentifiers) {
function baseCompile (line 16199) | function baseCompile(source, options = {}) {
constant V_MODEL_RADIO (line 16242) | const V_MODEL_RADIO = Symbol(`vModelRadio` );
constant V_MODEL_CHECKBOX (line 16243) | const V_MODEL_CHECKBOX = Symbol(`vModelCheckbox` );
constant V_MODEL_TEXT (line 16244) | const V_MODEL_TEXT = Symbol(`vModelText` );
constant V_MODEL_SELECT (line 16245) | const V_MODEL_SELECT = Symbol(`vModelSelect` );
constant V_MODEL_DYNAMIC (line 16246) | const V_MODEL_DYNAMIC = Symbol(`vModelDynamic` );
constant V_ON_WITH_MODIFIERS (line 16247) | const V_ON_WITH_MODIFIERS = Symbol(`vOnModifiersGuard` );
constant V_ON_WITH_KEYS (line 16248) | const V_ON_WITH_KEYS = Symbol(`vOnKeysGuard` );
constant V_SHOW (line 16249) | const V_SHOW = Symbol(`vShow` );
constant TRANSITION (line 16250) | const TRANSITION = Symbol(`Transition` );
constant TRANSITION_GROUP (line 16251) | const TRANSITION_GROUP = Symbol(`TransitionGroup` );
function decodeHtmlBrowser (line 16266) | function decodeHtmlBrowser(raw, asAttr = false) {
method getNamespace (line 16293) | getNamespace(tag, parent, rootNamespace) {
function createDOMCompilerError (line 16351) | function createDOMCompilerError(code, loc) {
function checkDuplicatedValue (line 16435) | function checkDuplicatedValue() {
function hasMultipleChildren (line 16646) | function hasMultipleChildren(node) {
function compile (line 16680) | function compile(src, options = {}) {
function getCache (line 16706) | function getCache(options) {
function compileToFunction (line 16714) | function compileToFunction(template, options) {
FILE: src/crusader-lib/assets/vue.prod.js
function c (line 5) | function c(e,t){let n=new Set(e.split(","));return t?e=>n.has(e.toLowerC...
function Z (line 5) | function Z(e){if(S(e)){let t={};for(let n=0;n<e.length;n++){let r=e[n],i...
function en (line 5) | function en(e){let t={};return e.replace(et,"").split(Y).forEach(e=>{if(...
function er (line 5) | function er(e){let t="";if(E(e))t=e;else if(S(e))for(let n=0;n<e.length;...
function ei (line 5) | function ei(e){if(!e)return null;let{class:t,style:n}=e;return t&&!E(t)&...
function eu (line 5) | function eu(e,t){if(e===t)return!0;let n=T(e),r=T(t);if(n||r)return!!n&&...
function ed (line 5) | function ed(e,t){return e.findIndex(e=>eu(e,t))}
class eg (line 5) | class eg{constructor(e=!1){this.detached=e,this._active=!0,this.effects=...
method constructor (line 5) | constructor(e=!1){this.detached=e,this._active=!0,this.effects=[],this...
method active (line 5) | get active(){return this._active}
method run (line 5) | run(e){if(this._active){let n=t;try{return t=this,e()}finally{t=n}}}
method on (line 5) | on(){t=this}
method off (line 5) | off(){t=this.parent}
method stop (line 5) | stop(e){if(this._active){let t,n;for(t=0,n=this.effects.length;t<n;t++...
function ey (line 5) | function ey(e){return new eg(e)}
function ev (line 5) | function ev(e,n=t){n&&n.active&&n.effects.push(e)}
function eb (line 5) | function eb(){return t}
function e_ (line 5) | function e_(e){t&&t.cleanups.push(e)}
class eS (line 5) | class eS{constructor(e,t,n,r){this.fn=e,this.trigger=t,this.scheduler=n,...
method constructor (line 5) | constructor(e,t,n,r){this.fn=e,this.trigger=t,this.scheduler=n,this.ac...
method dirty (line 5) | get dirty(){if(2===this._dirtyLevel||3===this._dirtyLevel){this._dirty...
method dirty (line 5) | set dirty(e){this._dirtyLevel=e?4:0}
method run (line 5) | run(){if(this._dirtyLevel=0,!this.active)return this.fn();let e=eE,t=n...
method stop (line 5) | stop(){this.active&&(ex(this),eC(this),this.onStop&&this.onStop(),this...
function ex (line 5) | function ex(e){e._trackId++,e._depsLength=0}
function eC (line 5) | function eC(e){if(e.deps.length>e._depsLength){for(let t=e._depsLength;t...
function eT (line 5) | function eT(e,t){let n=e.get(t);void 0!==n&&t._trackId!==n&&(e.delete(t)...
function ek (line 5) | function ek(e,t){e.effect instanceof eS&&(e=e.effect.fn);let n=new eS(e,...
function ew (line 5) | function ew(e){e.effect.stop()}
function eI (line 5) | function eI(){eN.push(eE),eE=!1}
function eR (line 5) | function eR(){let e=eN.pop();eE=void 0===e||e}
function eO (line 5) | function eO(){for(eA--;!eA&&eL.length;)eL.shift()()}
function eM (line 5) | function eM(e,t,n){if(t.get(e)!==e._trackId){t.set(e,e._trackId);let n=e...
function eP (line 5) | function eP(e,t,n){for(let n of(eA++,e.keys())){let r;n._dirtyLevel<t&&(...
function eB (line 5) | function eB(e,t,r){if(eE&&n){let t=eF.get(e);t||eF.set(e,t=new Map);let ...
function eU (line 5) | function eU(e,t,n,r,i,l){let s=eF.get(e);if(!s)return;let o=[];if("clear...
function eW (line 5) | function eW(e){A(e)||(e=String(e));let t=tC(this);return eB(t,"has",e),t...
class eK (line 5) | class eK{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get...
method constructor (line 5) | constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}
method get (line 5) | get(e,t,n){let r=this._isReadonly,i=this._isShallow;if("__v_isReactive...
class ez (line 5) | class ez extends eK{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t...
method constructor (line 5) | constructor(e=!1){super(!1,e)}
method set (line 5) | set(e,t,n,r){let i=e[t];if(!this._isShallow){let t=t_(i);if(tS(n)||t_(...
method deleteProperty (line 5) | deleteProperty(e,t){let n=_(e,t);e[t];let r=Reflect.deleteProperty(e,t...
method has (line 5) | has(e,t){let n=Reflect.has(e,t);return A(t)&&eH.has(t)||eB(e,"has",t),n}
method ownKeys (line 5) | ownKeys(e){return eB(e,"iterate",S(e)?"length":eV),Reflect.ownKeys(e)}
class eG (line 5) | class eG extends eK{constructor(e=!1){super(!0,e)}set(e,t){return!0}dele...
method constructor (line 5) | constructor(e=!1){super(!0,e)}
method set (line 5) | set(e,t){return!0}
method deleteProperty (line 5) | deleteProperty(e,t){return!0}
function e1 (line 5) | function e1(e,t,n=!1,r=!1){let i=tC(e=e.__v_raw),l=tC(t);n||(W(t,l)&&eB(...
function e2 (line 5) | function e2(e,t=!1){let n=this.__v_raw,r=tC(n),i=tC(e);return t||(W(e,i)...
function e3 (line 5) | function e3(e,t=!1){return e=e.__v_raw,t||eB(tC(e),"iterate",eV),Reflect...
function e6 (line 5) | function e6(e,t=!1){t||tS(e)||t_(e)||(e=tC(e));let n=tC(this);return e0(...
function e4 (line 5) | function e4(e,t,n=!1){n||tS(t)||t_(t)||(t=tC(t));let r=tC(this),{has:i,g...
function e8 (line 5) | function e8(e){let t=tC(this),{has:n,get:r}=e0(t),i=n.call(t,e);i||(e=tC...
function e5 (line 5) | function e5(){let e=tC(this),t=0!==e.size,n=e.clear();return t&&eU(e,"cl...
function e9 (line 5) | function e9(e,t){return function(n,r){let i=this,l=i.__v_raw,s=tC(l),o=t...
function e7 (line 5) | function e7(e,t,n){return function(...r){let i=this.__v_raw,l=tC(i),s=x(...
function te (line 5) | function te(e){return function(...t){return"delete"!==e&&("clear"===e?vo...
method get (line 5) | get(e){return e1(this,e)}
method size (line 5) | get size(){return e3(this)}
method get (line 5) | get(e){return e1(this,e,!1,!0)}
method size (line 5) | get size(){return e3(this)}
method add (line 5) | add(e){return e6.call(this,e,!0)}
method set (line 5) | set(e,t){return e4.call(this,e,t,!0)}
method get (line 5) | get(e){return e1(this,e,!0)}
method size (line 5) | get size(){return e3(this,!0)}
method has (line 5) | has(e){return e2.call(this,e,!0)}
method get (line 5) | get(e){return e1(this,e,!0,!0)}
method size (line 5) | get size(){return e3(this,!0)}
method has (line 5) | has(e){return e2.call(this,e,!0)}
function tl (line 5) | function tl(e,t){let n=t?e?ti:tr:e?tn:tt;return(t,r,i)=>"__v_isReactive"...
function tf (line 5) | function tf(e){return t_(e)?e:tv(e,!1,eJ,ts,tu)}
function tm (line 5) | function tm(e){return tv(e,!1,eQ,to,td)}
function tg (line 5) | function tg(e){return tv(e,!0,eX,ta,tp)}
function ty (line 5) | function ty(e){return tv(e,!0,eZ,tc,th)}
function tv (line 5) | function tv(e,t,n,r,i){if(!N(e)||e.__v_raw&&!(t&&e.__v_isReactive))retur...
function tb (line 5) | function tb(e){return t_(e)?tb(e.__v_raw):!!(e&&e.__v_isReactive)}
function t_ (line 5) | function t_(e){return!!(e&&e.__v_isReadonly)}
function tS (line 5) | function tS(e){return!!(e&&e.__v_isShallow)}
function tx (line 5) | function tx(e){return!!e&&!!e.__v_raw}
function tC (line 5) | function tC(e){let t=e&&e.__v_raw;return t?tC(t):e}
function tT (line 5) | function tT(e){return Object.isExtensible(e)&&z(e,"__v_skip",!0),e}
class tE (line 5) | class tE{constructor(e,t,n,r){this.getter=e,this._setter=t,this.dep=void...
method constructor (line 5) | constructor(e,t,n,r){this.getter=e,this._setter=t,this.dep=void 0,this...
method value (line 5) | get value(){let e=tC(this);return(!e._cacheable||e.effect.dirty)&&W(e....
method value (line 5) | set value(e){this._setter(e)}
method _dirty (line 5) | get _dirty(){return this.effect.dirty}
method _dirty (line 5) | set _dirty(e){this.effect.dirty=e}
function tA (line 5) | function tA(e){var t;eE&&n&&(e=tC(e),eM(n,null!=(t=e.dep)?t:e.dep=e$(()=...
function tN (line 5) | function tN(e,t=4,n,r){let i=(e=tC(e)).dep;i&&eP(i,t)}
function tI (line 5) | function tI(e){return!!(e&&!0===e.__v_isRef)}
function tR (line 5) | function tR(e){return tM(e,!1)}
function tO (line 5) | function tO(e){return tM(e,!0)}
function tM (line 5) | function tM(e,t){return tI(e)?e:new tL(e,t)}
class tL (line 5) | class tL{constructor(e,t){this.__v_isShallow=t,this.dep=void 0,this.__v_...
method constructor (line 5) | constructor(e,t){this.__v_isShallow=t,this.dep=void 0,this.__v_isRef=!...
method value (line 5) | get value(){return tA(this),this._value}
method value (line 5) | set value(e){let t=this.__v_isShallow||tS(e)||t_(e);W(e=t?e:tC(e),this...
function tP (line 5) | function tP(e){tN(e,4)}
function t$ (line 5) | function t$(e){return tI(e)?e.value:e}
function tF (line 5) | function tF(e){return w(e)?e():t$(e)}
function tD (line 5) | function tD(e){return tb(e)?e:new Proxy(e,tV)}
class tB (line 5) | class tB{constructor(e){this.dep=void 0,this.__v_isRef=!0;let{get:t,set:...
method constructor (line 5) | constructor(e){this.dep=void 0,this.__v_isRef=!0;let{get:t,set:n}=e(()...
method value (line 5) | get value(){return this._get()}
method value (line 5) | set value(e){this._set(e)}
function tU (line 5) | function tU(e){return new tB(e)}
function tj (line 5) | function tj(e){let t=S(e)?Array(e.length):{};for(let n in e)t[n]=tK(e,n)...
class tH (line 5) | class tH{constructor(e,t,n){this._object=e,this._key=t,this._defaultValu...
method constructor (line 5) | constructor(e,t,n){this._object=e,this._key=t,this._defaultValue=n,thi...
method value (line 5) | get value(){let e=this._object[this._key];return void 0===e?this._defa...
method value (line 5) | set value(e){this._object[this._key]=e}
method dep (line 5) | get dep(){return function(e,t){let n=eF.get(e);return n&&n.get(t)}(tC(...
class tq (line 5) | class tq{constructor(e){this._getter=e,this.__v_isRef=!0,this.__v_isRead...
method constructor (line 5) | constructor(e){this._getter=e,this.__v_isRef=!0,this.__v_isReadonly=!0}
method value (line 5) | get value(){return this._getter()}
function tW (line 5) | function tW(e,t,n){return tI(e)?e:w(e)?new tq(e):N(e)&&arguments.length>...
function tK (line 5) | function tK(e,t,n){let r=e[t];return tI(r)?r:new tH(e,t,n)}
function tJ (line 5) | function tJ(e,t){}
function tQ (line 5) | function tQ(e,t,n,r){try{return r?e(...r):e()}catch(e){tY(e,t,n)}}
function tZ (line 5) | function tZ(e,t,n,r){if(w(e)){let i=tQ(e,t,n,r);return i&&I(i)&&i.catch(...
function tY (line 5) | function tY(e,t,n,r=!0){if(t&&t.vnode,t){let r=t.parent,i=t.proxy,l=`htt...
function t7 (line 5) | function t7(e){let t=t9||t5;return e?t.then(this?e.bind(this):e):t}
function ne (line 5) | function ne(e){t2.length&&t2.includes(e,t0&&e.allowRecurse?t3+1:t3)||(nu...
function nt (line 5) | function nt(){t0||t1||(t1=!0,t9=t5.then(function e(t){t1=!1,t0=!0,t2.sor...
function nn (line 5) | function nn(e){S(e)?t6.push(...e):t4&&t4.includes(e,e.allowRecurse?t8+1:...
function nr (line 5) | function nr(e,t,n=t0?t3+1:0){for(;n<t2.length;n++){let t=t2[n];if(t&&t.p...
function ni (line 5) | function ni(e){if(t6.length){let e=[...new Set(t6)].sort((e,t)=>nl(e)-nl...
function nc (line 5) | function nc(e){let t=no;return no=e,na=e&&e.type.__scopeId||null,t}
function nu (line 5) | function nu(e){na=e}
function nd (line 5) | function nd(){na=null}
function nh (line 5) | function nh(e,t=no,n){if(!t||e._n)return e;let r=(...n)=>{let i;r._d&&iY...
function nf (line 5) | function nf(e,t){if(null===no)return e;let n=lT(no),r=e.dirs||(e.dirs=[]...
function nm (line 5) | function nm(e,t,n,r){let i=e.dirs,l=t&&t.dirs;for(let s=0;s<i.length;s++...
function nv (line 5) | function nv(){let e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVN...
method setup (line 5) | setup(e,{slots:t}){let n=lh(),r=nv();return()=>{let i=t.default&&nA(t.de...
function nC (line 5) | function nC(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Obj...
function nT (line 5) | function nT(e,t,n,r,i){let{appear:l,mode:s,persisted:o=!1,onBeforeEnter:...
function nk (line 5) | function nk(e){if(nM(e))return(e=lt(e)).children=null,e}
function nw (line 5) | function nw(e){if(!nM(e))return e;let{shapeFlag:t,children:n}=e;if(n){if...
function nE (line 5) | function nE(e,t){6&e.shapeFlag&&e.component?nE(e.component.subTree,t):12...
function nA (line 5) | function nA(e,t=!1,n){let r=[],i=0;for(let l=0;l<e.length;l++){let s=e[l...
function nN (line 5) | function nN(e,t){return w(e)?g({name:e.name},t,{setup:e}):e}
function nR (line 5) | function nR(e){let t;w(e)&&(e={loader:e});let{loader:n,loadingComponent:...
function nO (line 5) | function nO(e,t){let{ref:n,props:r,children:i,ce:l}=t.vnode,s=i7(e,r,i);...
method setup (line 5) | setup(e,{slots:t}){let n=lh(),r=n.ctx,i=new Map,l=new Set,s=null,o=n.sus...
function nP (line 5) | function nP(e,t){return S(e)?e.some(e=>nP(e,t)):E(e)?e.split(",").includ...
function n$ (line 5) | function n$(e,t){nV(e,"a",t)}
function nF (line 5) | function nF(e,t){nV(e,"da",t)}
function nV (line 5) | function nV(e,t,n=lp){let r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t...
function nD (line 5) | function nD(e){e.shapeFlag&=-257,e.shapeFlag&=-513}
function nB (line 5) | function nB(e){return 128&e.shapeFlag?e.ssContent:e}
function nU (line 5) | function nU(e,t,n=lp,r=!1){if(n){let i=n[e]||(n[e]=[]),l=t.__weh||(t.__w...
function nZ (line 5) | function nZ(e,t=lp){nU("ec",e,t)}
function n0 (line 5) | function n0(e,t){return n6(nY,e,!0,t)||e}
function n2 (line 5) | function n2(e){return E(e)?n6(nY,e,!1)||e:e||n1}
function n3 (line 5) | function n3(e){return n6("directives",e)}
function n6 (line 5) | function n6(e,t,n=!0,r=!1){let i=no||lp;if(i){let n=i.type;if(e===nY){le...
function n4 (line 5) | function n4(e,t){return e&&(e[t]||e[B(t)]||e[H(B(t))])}
function n8 (line 5) | function n8(e,t,n,r){let i;let l=n&&n[r];if(S(e)||E(e)){i=Array(e.length...
function n5 (line 5) | function n5(e,t){for(let n=0;n<t.length;n++){let r=t[n];if(S(r))for(let ...
function n9 (line 5) | function n9(e,t,n={},r,i){if(no.isCE||no.parent&&nI(no.parent)&&no.paren...
function n7 (line 5) | function n7(e,t){let n={};for(let r in e)n[t&&/[A-Z]/.test(r)?`on:${r}`:...
method get (line 5) | get({_:e},t){let n,r,i;if("__v_skip"===t)return!0;let{ctx:l,setupState:s...
method set (line 5) | set({_:e},t,n){let{data:r,setupState:i,ctx:l}=e;return rn(i,t)?(i[t]=n,!...
method has (line 5) | has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:i,propsOption...
method defineProperty (line 5) | defineProperty(e,t,n){return null!=n.get?e._.accessCache[t]=0:_(n,"value...
method get (line 5) | get(e,t){if(t!==Symbol.unscopables)return rr.get(e,t,e)}
function rl (line 5) | function rl(){return null}
function rs (line 5) | function rs(){return null}
function ro (line 5) | function ro(e){}
function ra (line 5) | function ra(e){}
function rc (line 5) | function rc(){return null}
function ru (line 5) | function ru(){}
function rd (line 5) | function rd(e,t){return null}
function rp (line 5) | function rp(){return rf().slots}
function rh (line 5) | function rh(){return rf().attrs}
function rf (line 5) | function rf(){let e=lh();return e.setupContext||(e.setupContext=lC(e))}
function rm (line 5) | function rm(e){return S(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}
function rg (line 5) | function rg(e,t){let n=rm(e);for(let e in t){if(e.startsWith("__skip"))c...
function ry (line 5) | function ry(e,t){return e&&t?S(e)&&S(t)?e.concat(t):g({},rm(e),rm(t)):e||t}
function rv (line 5) | function rv(e,t){let n={};for(let r in e)t.includes(r)||Object.definePro...
function rb (line 5) | function rb(e){let t=lh(),n=e();return lm(),I(n)&&(n=n.catch(e=>{throw l...
function rS (line 5) | function rS(e,t,n){tZ(S(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}
function rx (line 5) | function rx(e){let t;let n=e.type,{mixins:r,extends:i}=n,{mixins:l,optio...
function rC (line 5) | function rC(e,t,n,r=!1){let{mixins:i,extends:l}=t;for(let s in l&&rC(e,l...
function rk (line 5) | function rk(e,t){return t?e?function(){return g(w(e)?e.call(this,this):e...
function rw (line 5) | function rw(e){if(S(e)){let t={};for(let n=0;n<e.length;n++)t[e[n]]=e[n]...
function rE (line 5) | function rE(e,t){return e?[...new Set([].concat(e,t))]:t}
function rA (line 5) | function rA(e,t){return e?g(Object.create(null),e,t):t}
function rN (line 5) | function rN(e,t){return e?S(e)&&S(t)?[...new Set([...e,...t])]:g(Object....
function rI (line 5) | function rI(){return{app:null,config:{isNativeTag:h,performance:!1,globa...
function rM (line 5) | function rM(e,t){if(lp){let n=lp.provides,r=lp.parent&&lp.parent.provide...
function rL (line 5) | function rL(e,t,n=!1){let r=lp||no;if(r||rO){let i=r?null==r.parent?r.vn...
function rP (line 5) | function rP(){return!!(lp||no||rO)}
function rD (line 5) | function rD(e,t,n,r){let i;let[l,s]=e.propsOptions,o=!1;if(t)for(let a i...
function rB (line 5) | function rB(e,t,n,r,i,l){let s=e[n];if(null!=s){let e=_(s,"default");if(...
function rj (line 5) | function rj(e){return!("$"===e[0]||$(e))}
function rQ (line 5) | function rQ(e,t,n,r,i=!1){if(S(e)){e.forEach((e,l)=>rQ(e,t&&(S(t)?t[l]:t...
function r6 (line 5) | function r6(e,t,n,{o:{insert:r},m:i},l=2){0===l&&r(e.targetAnchor,t,n);l...
method process (line 5) | process(e,t,n,r,i,l,s,o,a,c){let{mc:u,pc:d,pbc:p,o:{insert:h,querySelect...
method remove (line 5) | remove(e,t,n,{um:r,o:{remove:i}},l){let{shapeFlag:s,children:o,anchor:a,...
function r8 (line 5) | function r8(e){let t=e.ctx;if(t&&t.ut){let n=e.children[0].el;for(;n&&n!...
function r5 (line 5) | function r5(e,t,n,r){let i=t.targetStart=n(""),l=t.targetAnchor=n("");re...
function il (line 5) | function il(e){let{mt:t,p:n,o:{patchProp:r,createText:i,nextSibling:l,pa...
function io (line 5) | function io(e){return ic(e)}
function ia (line 5) | function ia(e){return ic(e,il)}
function ic (line 5) | function ic(e,t){var n;let r,l;X().__VUE__=!0;let{insert:s,remove:o,patc...
function iu (line 5) | function iu({type:e,props:t},n){return"svg"===n&&"foreignObject"===e||"m...
function id (line 5) | function id({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}
function ip (line 5) | function ip(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}
function ih (line 5) | function ih(e,t,n=!1){let r=e.children,i=t.children;if(S(r)&&S(i))for(le...
function im (line 5) | function im(e){if(e)for(let t=0;t<e.length;t++)e[t].active=!1}
function iv (line 5) | function iv(e,t){return iC(e,null,t)}
function ib (line 5) | function ib(e,t){return iC(e,null,{flush:"post"})}
function i_ (line 5) | function i_(e,t){return iC(e,null,{flush:"sync"})}
function ix (line 5) | function ix(e,t,n){return iC(e,t,n)}
function iC (line 5) | function iC(e,t,{immediate:n,deep:r,flush:i,once:l,onTrack:s,onTrigger:o...
function iT (line 5) | function iT(e,t,n){let r;let i=this.proxy,l=E(e)?e.includes(".")?ik(i,e)...
function ik (line 5) | function ik(e,t){let n=t.split(".");return()=>{let t=e;for(let e=0;e<n.l...
function iw (line 5) | function iw(e,t=1/0,n){if(t<=0||!N(e)||e.__v_skip||(n=n||new Set).has(e)...
function iE (line 5) | function iE(e,t,n=u){let r=lh(),i=B(t),l=j(t),s=iA(e,t),o=tU((s,o)=>{let...
function iN (line 5) | function iN(e,t,...n){let r;if(e.isUnmounted)return;let i=e.vnode.props|...
function iI (line 5) | function iI(e,t){return!!(e&&f(t))&&(_(e,(t=t.slice(2).replace(/Once$/,"...
function iR (line 5) | function iR(e){let t,n;let{type:r,vnode:i,proxy:l,withProxy:s,propsOptio...
function iL (line 5) | function iL(e,t,n){let r=Object.keys(t);if(r.length!==Object.keys(e).len...
function iP (line 5) | function iP({vnode:e,parent:t},n){for(;t;){let r=t.subTree;if(r.suspense...
method process (line 5) | process(e,t,n,r,i,l,s,o,a,c){if(null==e)!function(e,t,n,r,i,l,s,o,a){let...
function iD (line 5) | function iD(e,t){let n=e.props&&e.props[t];w(n)&&n()}
function iB (line 5) | function iB(e,t,n,r,i,l,s,o,a,c,u=!1){let d;let{p:p,m:h,um:f,n:m,o:{pare...
function iU (line 5) | function iU(e){let t;if(w(e)){let n=iZ&&e._c;n&&(e._d=!1,iX()),e=e(),n&&...
function ij (line 5) | function ij(e,t){t&&t.pendingBranch?S(e)?t.effects.push(...e):t.effects....
function iH (line 5) | function iH(e,t){e.activeBranch=t;let{vnode:n,parentComponent:r}=e,i=t.e...
function iX (line 5) | function iX(e=!1){iG.push(iJ=e?null:[])}
function iQ (line 5) | function iQ(){iG.pop(),iJ=iG[iG.length-1]||null}
function iY (line 5) | function iY(e){iZ+=e,e<0&&iJ&&(iJ.hasOnce=!0)}
function i0 (line 5) | function i0(e){return e.dynamicChildren=iZ>0?iJ||d:null,iQ(),iZ>0&&iJ&&i...
function i1 (line 5) | function i1(e,t,n,r,i,l){return i0(i9(e,t,n,r,i,l,!0))}
function i2 (line 5) | function i2(e,t,n,r,i){return i0(i7(e,t,n,r,i,!0))}
function i3 (line 5) | function i3(e){return!!e&&!0===e.__v_isVNode}
function i6 (line 5) | function i6(e,t){return e.type===t.type&&e.key===t.key}
function i4 (line 5) | function i4(e){}
function i9 (line 5) | function i9(e,t=null,n=null,r=0,i=null,l=e===iq?0:1,s=!1,o=!1){let a={__...
function le (line 5) | function le(e){return e?tx(e)||rV(e)?g({},e):e:null}
function lt (line 5) | function lt(e,t,n=!1,r=!1){let{props:i,ref:l,patchFlag:s,children:o,tran...
function ln (line 5) | function ln(e=" ",t=0){return i7(iW,null,e,t)}
function lr (line 5) | function lr(e,t){let n=i7(iz,null,e);return n.staticCount=t,n}
function li (line 5) | function li(e="",t=!1){return t?(iX(),i2(iK,null,e)):i7(iK,null,e)}
function ll (line 5) | function ll(e){return null==e||"boolean"==typeof e?i7(iK):S(e)?i7(iq,nul...
function ls (line 5) | function ls(e){return null===e.el&&-1!==e.patchFlag||e.memo?e:lt(e)}
function lo (line 5) | function lo(e,t){let n=0,{shapeFlag:r}=e;if(null==t)t=null;else if(S(t))...
function la (line 5) | function la(...e){let t={};for(let n=0;n<e.length;n++){let r=e[n];for(le...
function lc (line 5) | function lc(e,t,n,r=null){tZ(e,t,7,[n,r])}
function lg (line 5) | function lg(e){return 4&e.vnode.shapeFlag}
function lv (line 5) | function lv(e,t,n){w(t)?e.render=t:N(t)&&(e.setupState=tD(t)),lS(e,n)}
function lb (line 5) | function lb(e){l=e,s=e=>{e.render._rc&&(e.withProxy=new Proxy(e.ctx,ri))}}
function lS (line 5) | function lS(e,t,n){let r=e.type;if(!e.render){if(!t&&l&&!r.render){let t...
function lC (line 5) | function lC(e){return{attrs:new Proxy(e.attrs,lx),slots:e.slots,emit:e.e...
function lT (line 5) | function lT(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(...
function lk (line 5) | function lk(e,t=!0){return w(e)?e.displayName||e.name:e.name||t&&e.__name}
function lE (line 5) | function lE(e,t,n){let r=arguments.length;return 2!==r?(r>3?n=Array.prot...
function lA (line 5) | function lA(){}
function lN (line 5) | function lN(e,t,n,r){let i=n[r];if(i&&lI(i,e))return i;let l=t();return ...
function lI (line 5) | function lI(e,t){let n=e.memo;if(n.length!=t.length)return!1;for(let e=0...
function lX (line 5) | function lX(e){let t={};for(let n in e)n in lK||(t[n]=e[n]);if(!1===e.cs...
function lQ (line 5) | function lQ(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.add(t)),(e[lq]...
function lZ (line 5) | function lZ(e,t){t.split(/\s+/).forEach(t=>t&&e.classList.remove(t));let...
function lY (line 5) | function lY(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}
function l1 (line 5) | function l1(e,t,n,r){let i=e._endId=++l0,l=()=>{i===e._endId&&r()};if(n)...
function l2 (line 5) | function l2(e,t){let n=window.getComputedStyle(e),r=e=>(n[e]||"").split(...
function l3 (line 5) | function l3(e,t){for(;e.length<t.length;)e=e.concat(e);return Math.max(....
function l6 (line 5) | function l6(e){return"auto"===e?0:1e3*Number(e.slice(0,-1).replace(",","...
function l4 (line 5) | function l4(){return document.body.offsetHeight}
method beforeMount (line 5) | beforeMount(e,{value:t},{transition:n}){e[l8]="none"===e.style.display?"...
method mounted (line 5) | mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)}
method updated (line 5) | updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnt...
method beforeUnmount (line 5) | beforeUnmount(e,{value:t}){l7(e,t)}
function l7 (line 5) | function l7(e,t){e.style.display=t?e[l8]:"none",e[l5]=!t}
function st (line 5) | function st(e){let t=lh();if(!t)return;let n=t.ut=(n=e(t.proxy))=>{Array...
function sn (line 5) | function sn(e,t){if(1===e.nodeType){let n=e.style,r="";for(let e in t)n....
function sl (line 5) | function sl(e,t,n){if(S(n))n.forEach(n=>sl(e,t,n));else if(null==n&&(n="...
function sc (line 5) | function sc(e,t,n,r,i,l=ec(t)){r&&t.startsWith("xlink:")?null==n?e.remov...
function su (line 5) | function su(e,t,n,r){e.addEventListener(t,n,r)}
function sy (line 5) | function sy(e,t,n){let r=nN(e,t);class i extends s_{constructor(e){super...
class s_ (line 5) | class s_ extends sb{constructor(e,t={},n){super(),this._def=e,this._prop...
method constructor (line 5) | constructor(e,t={},n){super(),this._def=e,this._props=t,this._instance...
method connectedCallback (line 5) | connectedCallback(){this._connected=!0,this._instance||(this._resolved...
method disconnectedCallback (line 5) | disconnectedCallback(){this._connected=!1,t7(()=>{this._connected||(th...
method _resolveDef (line 5) | _resolveDef(){this._resolved=!0;for(let e=0;e<this.attributes.length;e...
method _resolveProps (line 5) | _resolveProps(e){let{props:t}=e,n=S(t)?t:Object.keys(t||{});for(let e ...
method _setAttr (line 5) | _setAttr(e){let t=this.hasAttribute(e)?this.getAttribute(e):void 0,n=B...
method _getProp (line 5) | _getProp(e){return this._props[e]}
method _setProp (line 5) | _setProp(e,t,n=!0,r=!0){t!==this._props[e]&&(this._props[e]=t,r&&this....
method _update (line 5) | _update(){sY(this._createVNode(),this.shadowRoot)}
method _createVNode (line 5) | _createVNode(){let e=i7(this._def,g({},this._props));return this._inst...
method _applyStyles (line 5) | _applyStyles(e){e&&e.forEach(e=>{let t=document.createElement("style")...
function sS (line 5) | function sS(e="$style"){{let t=lh();if(!t)return u;let n=t.type.__cssMod...
method setup (line 5) | setup(e,{slots:t}){let n,r;let i=lh(),l=nv();return nK(()=>{if(!n.length...
function sA (line 5) | function sA(e){let t=e.el;t[sT]&&t[sT](),t[sk]&&t[sk]()}
function sN (line 5) | function sN(e){sC.set(e,e.el.getBoundingClientRect())}
function sI (line 5) | function sI(e){let t=sx.get(e),n=sC.get(e),r=t.left-n.left,i=t.top-n.top...
function sO (line 5) | function sO(e){e.target.composing=!0}
function sM (line 5) | function sM(e){let t=e.target;t.composing&&(t.composing=!1,t.dispatchEve...
method created (line 5) | created(e,{modifiers:{lazy:t,trim:n,number:r}},i){e[sL]=sR(i);let l=r||i...
method mounted (line 5) | mounted(e,{value:t}){e.value=null==t?"":t}
method beforeUpdate (line 5) | beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:r,trim:i,number:l}},s...
method created (line 5) | created(e,t,n){e[sL]=sR(n),su(e,"change",()=>{let t=e._modelValue,n=sU(e...
method beforeUpdate (line 5) | beforeUpdate(e,t,n){e[sL]=sR(n),sF(e,t,n)}
function sF (line 5) | function sF(e,{value:t,oldValue:n},r){e._modelValue=t,S(t)?e.checked=ed(...
method created (line 5) | created(e,{value:t},n){e.checked=eu(t,n.props.value),e[sL]=sR(n),su(e,"c...
method beforeUpdate (line 5) | beforeUpdate(e,{value:t,oldValue:n},r){e[sL]=sR(r),t!==n&&(e.checked=eu(...
method created (line 5) | created(e,{value:t,modifiers:{number:n}},r){let i=C(t);su(e,"change",()=...
method mounted (line 5) | mounted(e,{value:t}){sB(e,t)}
method beforeUpdate (line 5) | beforeUpdate(e,t,n){e[sL]=sR(n)}
method updated (line 5) | updated(e,{value:t}){e._assigning||sB(e,t)}
function sB (line 5) | function sB(e,t,n){let r=e.multiple,i=S(t);if(!r||i||C(t)){for(let n=0,l...
function sU (line 5) | function sU(e){return"_value"in e?e._value:e.value}
function sj (line 5) | function sj(e,t){let n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}
method created (line 5) | created(e,t,n){sq(e,t,n,null,"created")}
method mounted (line 5) | mounted(e,t,n){sq(e,t,n,null,"mounted")}
method beforeUpdate (line 5) | beforeUpdate(e,t,n,r){sq(e,t,n,r,"beforeUpdate")}
method updated (line 5) | updated(e,t,n,r){sq(e,t,n,r,"updated")}
function sq (line 5) | function sq(e,t,n,r,i){let l=function(e,t){switch(e){case"SELECT":return...
method setScopeId (line 5) | setScopeId(e,t){e.setAttribute(t,"")}
method insertStaticContent (line 5) | insertStaticContent(e,t,n,r,i,l){let s=n?n.previousSibling:t.lastChild;i...
function sZ (line 5) | function sZ(){return o=sQ?o:ia(sX),sQ=!0,o}
function s3 (line 5) | function s3(e){return e instanceof SVGElement?"svg":"function"==typeof M...
function s6 (line 5) | function s6(e){return E(e)?document.querySelector(e):e}
function oV (line 5) | function oV(e,t,n,r,i,l,s,o=!1,a=!1,c=!1,u=oF){return e&&(o?(e.helper(on...
function oD (line 5) | function oD(e,t=oF){return{type:17,loc:t,elements:e}}
function oB (line 5) | function oB(e,t=oF){return{type:15,loc:t,properties:e}}
function oU (line 5) | function oU(e,t){return{type:16,loc:oF,key:E(e)?oj(e,!0):e,value:t}}
function oj (line 5) | function oj(e,t=!1,n=oF,r=0){return{type:4,loc:n,content:e,isStatic:t,co...
function oH (line 5) | function oH(e,t=oF){return{type:8,loc:t,children:e}}
function oq (line 5) | function oq(e,t=[],n=oF){return{type:14,loc:n,callee:e,arguments:t}}
function oW (line 5) | function oW(e,t,n=!1,r=!1,i=oF){return{type:18,params:e,returns:t,newlin...
function oK (line 5) | function oK(e,t,n,r=!0){return{type:19,test:e,consequent:t,alternate:n,n...
function oz (line 5) | function oz(e,{helper:t,removeHelper:n,inSSR:r}){if(!e.isBlock){var i,l;...
function oX (line 5) | function oX(e){return e>=97&&e<=122||e>=65&&e<=90}
function oQ (line 5) | function oQ(e){return 32===e||10===e||9===e||12===e||13===e}
function oZ (line 5) | function oZ(e){return 47===e||62===e||oQ(e)}
function oY (line 5) | function oY(e){let t=new Uint8Array(e.length);for(let n=0;n<e.length;n++...
function o1 (line 5) | function o1(e){throw e}
function o2 (line 5) | function o2(e){}
function o3 (line 5) | function o3(e,t,n,r){let i=SyntaxError(String(`https://vuejs.org/error-r...
function o4 (line 5) | function o4(e){switch(e){case"Teleport":case"teleport":return s9;case"Su...
function an (line 5) | function an(e,t,n=!1){for(let r=0;r<e.props.length;r++){let i=e.props[r]...
function ar (line 5) | function ar(e,t,n=!1,r=!1){for(let i=0;i<e.props.length;i++){let l=e.pro...
function ai (line 5) | function ai(e,t){return!!(e&&o6(e)&&e.content===t)}
function al (line 5) | function al(e){return 5===e.type||2===e.type}
function as (line 5) | function as(e){return 7===e.type&&"slot"===e.name}
function ao (line 5) | function ao(e){return 1===e.type&&3===e.tagType}
function aa (line 5) | function aa(e){return 1===e.type&&2===e.tagType}
function au (line 5) | function au(e,t,n){let r,i;let l=13===e.type?e.props:e.arguments[2],s=[]...
function ad (line 5) | function ad(e,t){let n=!1;if(4===e.key.type){let r=e.key.content;n=t.pro...
function ap (line 5) | function ap(e,t){return`_${t}_${e.replace(/[^\w]/g,(t,n)=>"-"===t?"_":e....
method constructor (line 5) | constructor(e,t){this.stack=e,this.cbs=t,this.state=1,this.buffer="",thi...
method inSFCRoot (line 5) | get inSFCRoot(){return 2===this.mode&&0===this.stack.length}
method reset (line 5) | reset(){this.state=1,this.mode=0,this.buffer="",this.sectionStart=0,this...
method getPos (line 5) | getPos(e){let t=1,n=e+1;for(let r=this.newlines.length-1;r>=0;r--){let i...
method peek (line 5) | peek(){return this.buffer.charCodeAt(this.index+1)}
method stateText (line 5) | stateText(e){60===e?(this.index>this.sectionStart&&this.cbs.ontext(this....
method stateInterpolationOpen (line 5) | stateInterpolationOpen(e){if(e===this.delimiterOpen[this.delimiterIndex]...
method stateInterpolation (line 5) | stateInterpolation(e){e===this.delimiterClose[0]&&(this.state=4,this.del...
method stateInterpolationClose (line 5) | stateInterpolationClose(e){e===this.delimiterClose[this.delimiterIndex]?...
method stateSpecialStartSequence (line 5) | stateSpecialStartSequence(e){let t=this.sequenceIndex===this.currentSequ...
method stateInRCDATA (line 5) | stateInRCDATA(e){if(this.sequenceIndex===this.currentSequence.length){if...
method stateCDATASequence (line 5) | stateCDATASequence(e){e===o0.Cdata[this.sequenceIndex]?++this.sequenceIn...
method fastForwardTo (line 5) | fastForwardTo(e){for(;++this.index<this.buffer.length;){let t=this.buffe...
method stateInCommentLike (line 5) | stateInCommentLike(e){e===this.currentSequence[this.sequenceIndex]?++thi...
method startSpecial (line 5) | startSpecial(e,t){this.enterRCDATA(e,t),this.state=31}
method enterRCDATA (line 5) | enterRCDATA(e,t){this.inRCDATA=!0,this.currentSequence=e,this.sequenceIn...
method stateBeforeTagName (line 5) | stateBeforeTagName(e){33===e?(this.state=22,this.sectionStart=this.index...
method stateInTagName (line 5) | stateInTagName(e){oZ(e)&&this.handleTagName(e)}
method stateInSFCRootTagName (line 5) | stateInSFCRootTagName(e){if(oZ(e)){let t=this.buffer.slice(this.sectionS...
method handleTagName (line 5) | handleTagName(e){this.cbs.onopentagname(this.sectionStart,this.index),th...
method stateBeforeClosingTagName (line 5) | stateBeforeClosingTagName(e){oQ(e)||(62===e?(this.state=1,this.sectionSt...
method stateInClosingTagName (line 5) | stateInClosingTagName(e){(62===e||oQ(e))&&(this.cbs.onclosetag(this.sect...
method stateAfterClosingTagName (line 5) | stateAfterClosingTagName(e){62===e&&(this.state=1,this.sectionStart=this...
method stateBeforeAttrName (line 5) | stateBeforeAttrName(e){62===e?(this.cbs.onopentagend(this.index),this.in...
method handleAttrStart (line 5) | handleAttrStart(e){118===e&&45===this.peek()?(this.state=13,this.section...
method stateInSelfClosingTag (line 5) | stateInSelfClosingTag(e){62===e?(this.cbs.onselfclosingtag(this.index),t...
method stateInAttrName (line 5) | stateInAttrName(e){(61===e||oZ(e))&&(this.cbs.onattribname(this.sectionS...
method stateInDirName (line 5) | stateInDirName(e){61===e||oZ(e)?(this.cbs.ondirname(this.sectionStart,th...
method stateInDirArg (line 5) | stateInDirArg(e){61===e||oZ(e)?(this.cbs.ondirarg(this.sectionStart,this...
method stateInDynamicDirArg (line 5) | stateInDynamicDirArg(e){93===e?this.state=14:(61===e||oZ(e))&&(this.cbs....
method stateInDirModifier (line 5) | stateInDirModifier(e){61===e||oZ(e)?(this.cbs.ondirmodifier(this.section...
method handleAttrNameEnd (line 5) | handleAttrNameEnd(e){this.sectionStart=this.index,this.state=17,this.cbs...
method stateAfterAttrName (line 5) | stateAfterAttrName(e){61===e?this.state=18:47===e||62===e?(this.cbs.onat...
method stateBeforeAttrValue (line 5) | stateBeforeAttrValue(e){34===e?(this.state=19,this.sectionStart=this.ind...
method handleInAttrValue (line 5) | handleInAttrValue(e,t){(e===t||this.fastForwardTo(t))&&(this.cbs.onattri...
method stateInAttrValueDoubleQuotes (line 5) | stateInAttrValueDoubleQuotes(e){this.handleInAttrValue(e,34)}
method stateInAttrValueSingleQuotes (line 5) | stateInAttrValueSingleQuotes(e){this.handleInAttrValue(e,39)}
method stateInAttrValueNoQuotes (line 5) | stateInAttrValueNoQuotes(e){oQ(e)||62===e?(this.cbs.onattribdata(this.se...
method stateBeforeDeclaration (line 5) | stateBeforeDeclaration(e){91===e?(this.state=26,this.sequenceIndex=0):th...
method stateInDeclaration (line 5) | stateInDeclaration(e){(62===e||this.fastForwardTo(62))&&(this.state=1,th...
method stateInProcessingInstruction (line 5) | stateInProcessingInstruction(e){(62===e||this.fastForwardTo(62))&&(this....
method stateBeforeComment (line 5) | stateBeforeComment(e){45===e?(this.state=28,this.currentSequence=o0.Comm...
method stateInSpecialComment (line 5) | stateInSpecialComment(e){(62===e||this.fastForwardTo(62))&&(this.cbs.onc...
method stateBeforeSpecialS (line 5) | stateBeforeSpecialS(e){e===o0.ScriptEnd[3]?this.startSpecial(o0.ScriptEn...
method stateBeforeSpecialT (line 5) | stateBeforeSpecialT(e){e===o0.TitleEnd[3]?this.startSpecial(o0.TitleEnd,...
method startEntity (line 5) | startEntity(){}
method stateInEntity (line 5) | stateInEntity(){}
method parse (line 5) | parse(e){for(this.buffer=e;this.index<this.buffer.length;){let e=this.bu...
method cleanup (line 5) | cleanup(){this.sectionStart!==this.index&&(1===this.state||32===this.sta...
method finish (line 5) | finish(){this.handleTrailingData(),this.cbs.onend()}
method handleTrailingData (line 5) | handleTrailingData(){let e=this.buffer.length;this.sectionStart>=e||(28=...
method emitCodePoint (line 5) | emitCodePoint(e,t){}
method ontext (line 5) | ontext(e,t){aO(aI(e,t),e,t)}
method ontextentity (line 5) | ontextentity(e,t,n){aO(e,t,n)}
method oninterpolation (line 5) | oninterpolation(e,t){if(aT)return aO(aI(e,t),e,t);let n=e+aE.delimiterOp...
method onopentagname (line 5) | onopentagname(e,t){let n=aI(e,t);av={type:1,tag:n,ns:am.getNamespace(n,a...
method onopentagend (line 5) | onopentagend(e){aR(e)}
method onclosetag (line 5) | onclosetag(e,t){let n=aI(e,t);if(!am.isVoidTag(n)){let r=!1;for(let e=0;...
method onselfclosingtag (line 5) | onselfclosingtag(e){let t=av.tag;av.isSelfClosing=!0,aR(e),aw[0]&&aw[0]....
method onattribname (line 5) | onattribname(e,t){ab={type:6,name:aI(e,t),nameLoc:aB(e,t),value:void 0,l...
method ondirname (line 5) | ondirname(e,t){let n=aI(e,t),r="."===n||":"===n?"bind":"@"===n?"on":"#"=...
method ondirarg (line 5) | ondirarg(e,t){if(e===t)return;let n=aI(e,t);if(aT)ab.name+=n,aU(ab.nameL...
method ondirmodifier (line 5) | ondirmodifier(e,t){let n=aI(e,t);if(aT)ab.name+="."+n,aU(ab.nameLoc,t);e...
method onattribdata (line 5) | onattribdata(e,t){a_+=aI(e,t),aS<0&&(aS=e),ax=t}
method onattribentity (line 5) | onattribentity(e,t,n){a_+=e,aS<0&&(aS=t),ax=n}
method onattribnameend (line 5) | onattribnameend(e){let t=aI(ab.loc.start.offset,e);7===ab.type&&(ab.rawN...
method onattribend (line 5) | onattribend(e,t){av&&ab&&(aU(ab.loc,t),0!==e&&(a_.includes("&")&&(a_=am....
method oncomment (line 5) | oncomment(e,t){am.comments&&aD({type:3,content:aI(e,t),loc:aB(e-4,t+3)})}
method onend (line 5) | onend(){let e=ay.length;for(let t=0;t<aw.length;t++)aM(aw[t],e-1),aw[t]....
method oncdata (line 5) | oncdata(e,t){0!==aw[0].ns&&aO(aI(e,t),e,t)}
method onprocessinginstruction (line 5) | onprocessinginstruction(e){(aw[0]?aw[0].ns:am.ns)===0&&aH(21,e-1)}
function aI (line 5) | function aI(e,t){return ay.slice(e,t)}
function aR (line 5) | function aR(e){aE.inSFCRoot&&(av.innerLoc=aB(e+1,e+1)),aD(av);let{tag:t,...
function aO (line 5) | function aO(e,t,n){{let t=aw[0]&&aw[0].tag;"script"!==t&&"style"!==t&&e....
function aM (line 5) | function aM(e,t,n=!1){n?aU(e.loc,aL(t,60)):aU(e.loc,function(e,t){let n=...
function aL (line 5) | function aL(e,t){let n=e;for(;ay.charCodeAt(n)!==t&&n>=0;)n--;return n}
function aF (line 5) | function aF(e,t){let n="preserve"!==am.whitespace,r=!1;for(let t=0;t<e.l...
function aV (line 5) | function aV(e){let t="",n=!1;for(let r=0;r<e.length;r++)oQ(e.charCodeAt(...
function aD (line 5) | function aD(e){(aw[0]||ag).children.push(e)}
function aB (line 5) | function aB(e,t){return{start:aE.getPos(e),end:null==t?t:aE.getPos(t),so...
function aU (line 5) | function aU(e,t){e.end=aE.getPos(t),e.source=aI(e.start.offset,t)}
function aj (line 5) | function aj(e,t=!1,n,r=0,i=0){return oj(e,t,n,r)}
function aH (line 5) | function aH(e,t,n){am.onError(o3(e,aB(t,t)))}
function aq (line 5) | function aq(e,t){let{children:n}=e;return 1===n.length&&1===t.type&&!aa(t)}
function aW (line 5) | function aW(e,t){let{constantCache:n}=t;switch(e.type){case 1:if(0!==e.t...
function az (line 5) | function az(e,t){let n=3,r=aG(e);if(r&&15===r.type){let{properties:e}=r;...
function aG (line 5) | function aG(e){let t=e.codegenNode;if(13===t.type)return t.props}
function aJ (line 5) | function aJ(e,t){t.currentNode=e;let{nodeTransforms:n}=t,r=[];for(let i=...
function aX (line 5) | function aX(e,t){let n=E(e)?t=>t===e:t=>e.test(t);return(e,r)=>{if(1===e...
function aY (line 5) | function aY(e,t,{helper:n,push:r,newline:i,isTS:l}){let s=n("component"=...
function a0 (line 5) | function a0(e,t){let n=e.length>3;t.push("["),n&&t.indent(),a1(e,t,n),n&...
function a1 (line 5) | function a1(e,t,n=!1,r=!0){let{push:i,newline:l}=t;for(let s=0;s<e.lengt...
function a2 (line 5) | function a2(e,t){if(E(e)){t.push(e,-3);return}if(A(e)){t.push(t.helper(e...
function a3 (line 5) | function a3(e,t){let{content:n,isStatic:r}=e;t.push(r?JSON.stringify(n):...
function a6 (line 5) | function a6(e,t){for(let n=0;n<e.children.length;n++){let r=e.children[n...
function a8 (line 5) | function a8(e,t){let n=3===e.tagType;return{type:10,loc:e.loc,condition:...
function a5 (line 5) | function a5(e,t,n){return e.condition?oK(e.condition,a9(e,t,n),oq(n.help...
function a9 (line 5) | function a9(e,t,n){let{helper:r}=n,i=oU("key",oj(`${t}`,!1,oF,2)),{child...
function cr (line 5) | function cr(e,t){e.finalized||(e.finalized=!0)}
function ci (line 5) | function ci({value:e,key:t,index:n},r=[]){return function(e){let t=e.len...
function ca (line 5) | function ca(e,t,n){let r=[oU("name",e),oU("fn",t)];return null!=n&&r.pus...
function cd (line 5) | function cd(e,t,n=e.props,r,i,l=!1){let s;let{tag:o,loc:a,children:c}=e,...
function cp (line 5) | function cp(e){let t=new Map,n=[];for(let r=0;r<e.length;r++){let i=e[r]...
function ch (line 5) | function ch(e){return"component"===e||"Component"===e}
function cS (line 5) | function cS(e=[]){return{props:e}}
method getNamespace (line 5) | getNamespace(e,t,n){let r=t?t.ns:n;if(t&&2===r){if("annotation-xml"===t....
function cK (line 5) | function cK(e,t){let n;if(!E(e)){if(!e.nodeType)return p;e=e.innerHTML}l...
FILE: src/crusader-lib/build.rs
function main (line 3) | fn main() {
FILE: src/crusader-lib/src/common.rs
type Msg (line 37) | pub(crate) type Msg = Arc<dyn Fn(&str) + Send + Sync>;
type TestState (line 41) | pub(crate) enum TestState {
type Config (line 56) | pub struct Config {
function connect (line 69) | pub async fn connect<A: ToSocketAddrs>(addr: A, name: &str) -> Result<Tc...
function interface_ips (line 76) | pub fn interface_ips() -> Vec<(String, IpAddr)> {
function is_unicast_link_local (line 129) | pub fn is_unicast_link_local(ip: Ipv6Addr) -> bool {
function fresh_socket_addr (line 133) | pub fn fresh_socket_addr(socket: SocketAddr, port: u16) -> SocketAddr {
function inherit_local (line 145) | pub fn inherit_local(socket: SocketAddr, ip: IpAddr, port: u16) -> Socke...
function data (line 157) | pub(crate) fn data() -> Vec<u8> {
function read_data (line 169) | pub(crate) async fn read_data(
function write_data (line 262) | pub(crate) async fn write_data(
function hello (line 314) | pub(crate) async fn hello<
function udp_handle (line 342) | pub(crate) fn udp_handle(result: std::io::Result<()>) -> std::io::Result...
function ping_measure_send (line 355) | async fn ping_measure_send(
function ping_measure_recv (line 388) | async fn ping_measure_recv(
type LatencyResult (line 423) | pub struct LatencyResult {
function measure_latency (line 432) | pub(crate) async fn measure_latency(
function ping_send (line 560) | pub(crate) async fn ping_send(
function ping_recv (line 605) | pub(crate) async fn ping_recv(
function wait_for_state (line 645) | pub(crate) async fn wait_for_state(
FILE: src/crusader-lib/src/discovery.rs
constant DISCOVER_PORT (line 16) | pub const DISCOVER_PORT: u16 = protocol::PORT + 2;
constant DISCOVER_VERSION (line 17) | pub const DISCOVER_VERSION: u64 = 0;
type Hello (line 20) | struct Hello {
method new (line 26) | pub fn new() -> Self {
type Data (line 35) | struct Data {
type Message (line 41) | enum Message {
type Server (line 57) | pub struct Server {
function interfaces (line 63) | fn interfaces() -> Vec<u32> {
function locate (line 90) | pub async fn locate(peer_server: bool) -> Result<Server, anyhow::Error> {
function serve (line 221) | pub fn serve(state: Arc<State>, port: u16) -> Result<(), anyhow::Error> {
FILE: src/crusader-lib/src/file_format.rs
type Elasped (line 18) | pub struct Elasped {
type RawPingV0 (line 25) | pub struct RawPingV0 {
method to_v1 (line 32) | pub fn to_v1(&self) -> RawPing {
type RawConfigV0 (line 45) | pub struct RawConfigV0 {
method to_v1 (line 56) | pub fn to_v1(&self) -> RawConfig {
type RawResultV0 (line 68) | pub struct RawResultV0 {
method to_v1 (line 77) | pub fn to_v1(&self) -> RawResult {
type TestKind (line 97) | pub enum TestKind {
method name (line 104) | pub fn name(&self) -> &'static str {
type TestData (line 114) | pub struct TestData {
type RawPoint (line 121) | pub struct RawPoint {
type RawStream (line 127) | pub struct RawStream {
method to_vec (line 132) | pub(crate) fn to_vec(&self) -> Vec<(u64, u64)> {
type RawStreamGroup (line 141) | pub struct RawStreamGroup {
type RawPing (line 148) | pub struct RawPing {
type RawConfig (line 155) | pub struct RawConfig {
type RawHeader (line 165) | pub struct RawHeader {
method default (line 171) | fn default() -> Self {
type RawResult (line 180) | pub struct RawResult {
method streams (line 201) | pub fn streams(&self) -> u64 {
method download (line 210) | pub fn download(&self) -> bool {
method upload (line 216) | pub fn upload(&self) -> bool {
method idle (line 222) | pub fn idle(&self) -> bool {
method both (line 226) | pub fn both(&self) -> bool {
method load_from_reader (line 230) | pub fn load_from_reader(reader: impl Read) -> Option<Self> {
method load (line 249) | pub fn load(path: &Path) -> Option<Self> {
method save_to_writer (line 253) | pub fn save_to_writer(&self, writer: impl Write) -> Result<(), anyhow:...
method save (line 266) | pub fn save(&self, name: &Path) -> Result<(), anyhow::Error> {
FILE: src/crusader-lib/src/latency.rs
type UpdateFn (line 28) | type UpdateFn = Arc<dyn Fn() + Send + Sync>;
type Config (line 31) | pub struct Config {
type EventKind (line 37) | pub enum EventKind {
type Event (line 45) | pub struct Event {
type Point (line 51) | pub struct Point {
type State (line 62) | pub enum State {
type Data (line 68) | pub struct Data {
method new (line 77) | pub fn new(limit: usize, update_fn: UpdateFn) -> Self {
function test_async (line 88) | async fn test_async(
function ping_send (line 329) | async fn ping_send(
function ping_recv (line 380) | async fn ping_recv(
function test_callback (line 406) | pub fn test_callback(
FILE: src/crusader-lib/src/lib.rs
constant VERSION (line 10) | const VERSION: &str = "0.3.3-dev";
function version (line 12) | pub fn version() -> String {
function with_time (line 23) | pub fn with_time(msg: &str) -> String {
FILE: src/crusader-lib/src/peer.rs
type Peer (line 28) | pub struct Peer {
method start (line 36) | pub async fn start(&mut self) -> Result<(), anyhow::Error> {
method stop (line 58) | pub async fn stop(&mut self) -> Result<(), anyhow::Error> {
method complete (line 63) | pub async fn complete(mut self) -> Result<(bool, Vec<PeerLatency>), an...
function connect_to_peer (line 76) | pub async fn connect_to_peer(
function run_peer (line 135) | pub async fn run_peer(
FILE: src/crusader-lib/src/plot.rs
constant UP_COLOR (line 18) | const UP_COLOR: RGBColor = RGBColor(37, 83, 169);
constant DOWN_COLOR (line 19) | const DOWN_COLOR: RGBColor = RGBColor(95, 145, 62);
function darken (line 21) | fn darken(color: RGBColor, d: f64) -> RGBColor {
function register_fonts (line 29) | pub fn register_fonts() {
method to_test_result (line 40) | pub fn to_test_result(&self) -> TestResult {
type TestStream (line 262) | pub struct TestStream {
type TestStreamGroup (line 266) | pub struct TestStreamGroup {
type LatencySummary (line 273) | pub struct LatencySummary {
type LatencyLossSummary (line 280) | pub struct LatencyLossSummary {
type TestResult (line 285) | pub struct TestResult {
method summary (line 304) | pub fn summary(&self) -> Result<String, anyhow::Error> {
function save_graph (line 409) | pub fn save_graph(
function save_graph_to_path (line 421) | pub fn save_graph_to_path(
type ThroughputPlot (line 431) | pub(crate) struct ThroughputPlot<'a> {
function save_graph_to_mem (line 442) | pub(crate) fn save_graph_to_mem(
function float_max (line 548) | pub fn float_max(iter: impl Iterator<Item = f64>) -> f64 {
function to_float (line 558) | fn to_float(stream: &[(u64, u64)]) -> Vec<(u64, f64)> {
function to_rates (line 562) | pub fn to_rates(stream: &[(u64, f64)]) -> Vec<(u64, f64)> {
function throughput (line 588) | fn throughput(
function ping_peak (line 636) | fn ping_peak(
function ping_loss (line 678) | fn ping_loss(
function smooth (line 721) | pub fn smooth(
function smooth_ping (line 765) | fn smooth_ping(pings: &[RawPing], interval: Duration) -> Vec<RawPing> {
function sum_bytes (line 814) | fn sum_bytes(input: &[&[(u64, f64)]], interval: Duration) -> Vec<(u64, f...
function interpolate (line 854) | fn interpolate(input: &[(u64, f64)], interval: u64) -> Vec<(u64, f64)> {
function draw_centered (line 886) | fn draw_centered(
function new_chart (line 916) | fn new_chart<'a, 'c>(
function legends (line 956) | fn legends<'a, 'b: 'a>(
constant PACKET_LOSS_AREA_SIZE (line 970) | const PACKET_LOSS_AREA_SIZE: f64 = 70.0;
function latency (line 972) | fn latency<'a>(
function plot_split_throughput (line 1240) | fn plot_split_throughput(
function plot_throughput (line 1329) | fn plot_throughput(
function bytes_transferred (line 1431) | pub(crate) fn bytes_transferred(
function graph (line 1484) | pub(crate) fn graph(
FILE: src/crusader-lib/src/protocol.rs
type RawLatency (line 9) | pub struct RawLatency {
method down (line 16) | pub fn down(&self) -> Option<Duration> {
constant PORT (line 21) | pub const PORT: u16 = 35481;
constant MAGIC (line 23) | pub const MAGIC: u64 = 0x5372ab82ae7c59cb;
constant VERSION (line 24) | pub const VERSION: u64 = 3;
type Hello (line 27) | pub struct Hello {
method new (line 33) | pub fn new() -> Self {
type TestStream (line 42) | pub struct TestStream {
type LatencyMeasure (line 48) | pub struct LatencyMeasure {
type PeerLatency (line 54) | pub struct PeerLatency {
type ServerMessage (line 60) | pub enum ServerMessage {
type ClientMessage (line 96) | pub enum ClientMessage {
type Ping (line 132) | pub struct Ping {
function codec (line 137) | pub fn codec() -> LengthDelimitedCodec {
function send (line 144) | pub async fn send<S: Sink<Bytes> + Unpin>(
function receive (line 154) | pub async fn receive<S: Stream<Item = Result<BytesMut, E>> + Unpin, T: f...
FILE: src/crusader-lib/src/remote.rs
type Env (line 34) | struct Env {
function ws_client (line 39) | async fn ws_client(
type TestArgs (line 50) | struct TestArgs {
function handle_client (line 70) | async fn handle_client(
function listen (line 166) | async fn listen(state: Arc<Env>, listener: TcpListener) {
function serve_async (line 206) | async fn serve_async(port: u16, msg: Box<dyn Fn(&str) + Send + Sync>) ->...
function serve_until (line 263) | pub fn serve_until(
function run (line 290) | pub fn run(port: u16) -> Result<(), anyhow::Error> {
FILE: src/crusader-lib/src/serve.rs
type SlotUpdate (line 31) | struct SlotUpdate {
type Client (line 38) | struct Client {
method forward_latency_msgs (line 49) | fn forward_latency_msgs(&self) {
method load_waiter (line 63) | fn load_waiter(&self, group: u32) -> watch::Receiver<Option<Instant>> {
method schedule_loads (line 71) | async fn schedule_loads(
type Pong (line 95) | struct Pong {
type State (line 99) | pub(crate) struct State {
function ip_to_ipv6_mapped (line 109) | fn ip_to_ipv6_mapped(ip: IpAddr) -> Ipv6Addr {
type OnDrop (line 116) | pub struct OnDrop<F: Fn()>(pub F);
method drop (line 119) | fn drop(&mut self) {
function client (line 124) | async fn client(state: Arc<State>, stream: TcpStream) -> Result<(), anyh...
function listen (line 491) | async fn listen(state: Arc<State>, listener: TcpListener) {
function handle_ping (line 509) | async fn handle_ping(
function pong (line 551) | async fn pong(
constant SLOTS (line 600) | const SLOTS: usize = 1000;
function start_pong_server (line 602) | async fn start_pong_server(
function serve_async (line 626) | async fn serve_async(
function serve_until (line 680) | pub fn serve_until(
function serve (line 708) | pub fn serve(port: u16, peer_server: bool) -> Result<(), anyhow::Error> {
FILE: src/crusader-lib/src/test.rs
constant MEASURE_DELAY (line 43) | const MEASURE_DELAY: Duration = Duration::from_millis(50);
type ScheduledLoads (line 46) | struct ScheduledLoads;
type State (line 48) | struct State {
function hello_combined (line 53) | async fn hello_combined<S: Sink<Bytes> + Stream<Item = Result<BytesMut, ...
type PlotConfig (line 75) | pub struct PlotConfig {
function test_async (line 85) | pub(crate) async fn test_async(
function save_raw (line 680) | pub fn save_raw(
function setup_loaders (line 692) | fn setup_loaders(
function upload_loaders (line 714) | fn upload_loaders(
function wait_on_download_loaders (line 794) | async fn wait_on_download_loaders(
function download_loaders (line 813) | fn download_loaders(
function timed (line 939) | pub fn timed(name: &str) -> String {
function unique (line 944) | pub(crate) fn unique(name: &str, ext: &str, root_path: &Path) -> String {
function test (line 961) | pub fn test(
function test_callback (line 1000) | pub fn test_callback(
FILE: src/crusader/src/main.rs
type Cli (line 25) | struct Cli {
type PlotArgs (line 31) | struct PlotArgs {
method config (line 56) | fn config(&self) -> PlotConfig {
type Commands (line 70) | enum Commands {
function run (line 179) | fn run() -> Result<(), anyhow::Error> {
function main (line 283) | fn main() {
Condensed preview — 64 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,072K chars).
[
{
"path": ".dockerignore",
"chars": 11,
"preview": "/src/target"
},
{
"path": ".gitattributes",
"chars": 43,
"preview": "src/crusader-lib/assets/* linguist-vendored"
},
{
"path": ".github/workflows/ci.yml",
"chars": 2064,
"preview": "name: ci\r\n\r\non:\r\n push:\r\n branches: [master]\r\n pull_request:\r\n\r\nenv:\r\n CARGO_INCREMENTAL: 0\r\n\r\njobs:\r\n build:\r\n "
},
{
"path": ".github/workflows/release.md",
"chars": 117,
"preview": "Crusader has pre-built binaries for a number of operating systems. Download the appropriate binary below for your OS."
},
{
"path": ".github/workflows/release.yml",
"chars": 6895,
"preview": "name: release\r\non:\r\n push:\r\n tags:\r\n - \"v*\"\r\n\r\nenv:\r\n CARGO_INCREMENTAL: 0\r\n\r\njobs:\r\n create-release:\r\n name"
},
{
"path": ".gitignore",
"chars": 12,
"preview": "/src/target\n"
},
{
"path": "CHANGELOG.md",
"chars": 3790,
"preview": "# CHANGELOG\n\nThe **Crusader Network Tester** measures network rates and latency\nin the presence of upload and download t"
},
{
"path": "LICENSE-APACHE",
"chars": 9723,
"preview": " Apache License\n Version 2.0, January 2004\n http"
},
{
"path": "LICENSE-MIT",
"chars": 1023,
"preview": "Permission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentati"
},
{
"path": "README.md",
"chars": 4380,
"preview": "# Crusader Network Tester\n\n[](https://github.com"
},
{
"path": "android/.gitignore",
"chars": 48,
"preview": ".gradle\n/target\n/app/build\n/app/src/main/jniLibs"
},
{
"path": "android/Cargo.toml",
"chars": 997,
"preview": "[package]\nname = \"crusader-android\"\nversion = \"0.1.0\"\nedition = \"2021\"\nresolver = \"2\"\n\n# See more keys and their definit"
},
{
"path": "android/app/build.gradle",
"chars": 1392,
"preview": "plugins {\n id 'com.android.application'\n}\n\nandroid {\n compileSdk 31\n\n defaultConfig {\n applicationId \"zo"
},
{
"path": "android/app/src/main/AndroidManifest.xml",
"chars": 839,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"zox"
},
{
"path": "android/app/src/main/java/zoxc/crusader/MainActivity.java",
"chars": 4515,
"preview": "package zoxc.crusader;\n\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.core.view.WindowCompat;\nimport "
},
{
"path": "android/app/src/main/res/values/colors.xml",
"chars": 421,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"purple_200\">#FFBB86FC</color>\n <color name=\"purpl"
},
{
"path": "android/app/src/main/res/values/themes.xml",
"chars": 938,
"preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n <!-- Base application theme. -->\n <style name=\"Theme.R"
},
{
"path": "android/build.gradle",
"chars": 292,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nplugins {\n id 'co"
},
{
"path": "android/debugInstall.ps1",
"chars": 294,
"preview": "$ErrorActionPreference = \"Stop\"\ncargo ndk -t arm64-v8a -o app/src/main/jniLibs/ -- build --release\nif ($lastexitcode -ne"
},
{
"path": "android/gradle/wrapper/gradle-wrapper.properties",
"chars": 230,
"preview": "#Mon May 02 15:39:12 BST 2022\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributio"
},
{
"path": "android/gradle.properties",
"chars": 1267,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "android/gradlew",
"chars": 5766,
"preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
},
{
"path": "android/gradlew.bat",
"chars": 2674,
"preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
},
{
"path": "android/settings.gradle",
"chars": 296,
"preview": "pluginManagement {\n repositories {\n gradlePluginPortal()\n google()\n mavenCentral()\n }\n}\ndepen"
},
{
"path": "android/src/lib.rs",
"chars": 6667,
"preview": "#![allow(\n clippy::field_reassign_with_default,\n clippy::option_map_unit_fn,\n clippy::missing_safety_doc\n)]\n\nus"
},
{
"path": "docker/README.md",
"chars": 532,
"preview": "To build a statically linked server image:\r\n```\r\ndocker build .. -t crusader -f server-static.Dockerfile\r\n```\r\n\r\nTo buil"
},
{
"path": "docker/remote-static.Dockerfile",
"chars": 821,
"preview": "FROM rust AS build\r\nARG TARGETARCH\r\nARG PROFILE=release\r\n\r\nCOPY src /src\r\nWORKDIR /src\r\n\r\nRUN echo no-target-detected > "
},
{
"path": "docker/server-static.Dockerfile",
"chars": 862,
"preview": "FROM rust AS build\r\nARG TARGETARCH\r\nARG PROFILE=release\r\n\r\nCOPY src /src\r\nWORKDIR /src\r\n\r\nRUN echo no-target-detected > "
},
{
"path": "docs/BUILDING.md",
"chars": 982,
"preview": "# Building Crusader from source\n\nReminder: [Pre-built binaries](https://github.com/Zoxc/crusader/releases)\nare available"
},
{
"path": "docs/CLI.md",
"chars": 3197,
"preview": "# Running Crusader from the command line\n\n## Server\n\nTo host a Crusader server, run this on the _server_ machine:\n\n```sh"
},
{
"path": "docs/LOCAL_TESTS.md",
"chars": 4024,
"preview": "# Local network testing with Crusader\n\n**Background:**\nThe Crusader Network Tester measures network throughput\nand laten"
},
{
"path": "docs/RESULTS.md",
"chars": 3510,
"preview": "# Understanding Crusader Results\n\nThe Crusader GUI provides a compact summary of the test data.\nHere are some hints for "
},
{
"path": "docs/TROUBLESHOOTING.md",
"chars": 1009,
"preview": "# Troubleshooting\n\n* Crusader requires that TCP and UDP ports 35481 are open for its tests.\n Crusader also uses ports 3"
},
{
"path": "media/Crusader Screen Shots.md",
"chars": 1093,
"preview": "# Verify Crusader Screen Shots\n\nThis page is useful for checking the appearance of screen shots.\nCapture the screen shot"
},
{
"path": "media/batch_add_border.sh",
"chars": 1824,
"preview": "#!/bin/bash\n\n# add_border.sh - This script uses ImageMagick to process\n# macOS screen shots for publication\n\n# Usage: \n#"
},
{
"path": "src/Cargo.toml",
"chars": 440,
"preview": "[workspace]\r\nmembers = [\"crusader\", \"crusader-lib\", \"crusader-gui-lib\", \"crusader-gui\"]\r\nresolver = \"1\"\r\n\r\n[profile.dev]"
},
{
"path": "src/crusader/Cargo.toml",
"chars": 383,
"preview": "[package]\nname = \"crusader\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ncrusader-lib = { path = \"../crusader-lib\""
},
{
"path": "src/crusader/src/main.rs",
"chars": 9615,
"preview": "use anyhow::Context;\nuse clap::{Parser, Subcommand};\nuse clap_num::si_number;\n#[cfg(feature = \"client\")]\nuse crusader_li"
},
{
"path": "src/crusader-gui/Cargo.toml",
"chars": 337,
"preview": "[package]\nname = \"crusader-gui\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc"
},
{
"path": "src/crusader-gui/src/main.rs",
"chars": 3842,
"preview": "#![allow(\r\n clippy::field_reassign_with_default,\r\n clippy::option_map_unit_fn,\r\n clippy::type_complexity\r\n)]\r\n#"
},
{
"path": "src/crusader-gui-lib/Cargo.toml",
"chars": 647,
"preview": "[package]\nname = \"crusader-gui-lib\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https:/"
},
{
"path": "src/crusader-gui-lib/src/client.rs",
"chars": 20529,
"preview": "use crate::{Tab, Tester};\nuse crusader_lib::{\n file_format::RawResult,\n protocol,\n test::{self},\n with_time,"
},
{
"path": "src/crusader-gui-lib/src/lib.rs",
"chars": 75477,
"preview": "#![allow(\n clippy::field_reassign_with_default,\n clippy::option_map_unit_fn,\n clippy::type_complexity,\n clip"
},
{
"path": "src/crusader-lib/Cargo.toml",
"chars": 1294,
"preview": "[package]\nname = \"crusader-lib\"\nversion = \"0.1.0\"\nedition = \"2021\"\nbuild = \"build.rs\"\n\n# See more keys and their definit"
},
{
"path": "src/crusader-lib/UFL.txt",
"chars": 4769,
"preview": "-------------------------------\r\nUBUNTU FONT LICENCE Version 1.0\r\n-------------------------------\r\n\r\nPREAMBLE\r\nThis lice"
},
{
"path": "src/crusader-lib/assets/vue.js",
"chars": 493709,
"preview": "/**\n* vue v3.4.35\n* (c) 2018-present Yuxi (Evan) You and Vue contributors\n* @license MIT\n**/\n/*! #__NO_SIDE_EFFECTS__ */"
},
{
"path": "src/crusader-lib/assets/vue.prod.js",
"chars": 149917,
"preview": "/**\n* vue v3.4.35\n* (c) 2018-present Yuxi (Evan) You and Vue contributors\n* @license MIT\n**//*! #__NO_SIDE_EFFECTS__ */l"
},
{
"path": "src/crusader-lib/build.rs",
"chars": 324,
"preview": "use std::process::Command;\r\n\r\nfn main() {\r\n if let Some(commit) = Command::new(\"git\")\r\n .args([\"rev-parse\", \"-"
},
{
"path": "src/crusader-lib/src/common.rs",
"chars": 19232,
"preview": "use crate::{\r\n protocol::{receive, send, ClientMessage, Hello, Ping, ServerMessage},\r\n serve::OnDrop,\r\n};\r\nuse any"
},
{
"path": "src/crusader-lib/src/discovery.rs",
"chars": 8706,
"preview": "use crate::{common::is_unicast_link_local, protocol, serve::State, version};\r\n#[cfg(feature = \"client\")]\r\nuse anyhow::an"
},
{
"path": "src/crusader-lib/src/file_format.rs",
"chars": 7089,
"preview": "use serde::{Deserialize, Serialize};\r\nuse std::fs::File;\r\nuse std::io::BufReader;\r\nuse std::io::BufWriter;\r\nuse std::io:"
},
{
"path": "src/crusader-lib/src/latency.rs",
"chars": 13277,
"preview": "use anyhow::{anyhow, bail, Context};\r\nuse futures::future::FutureExt;\r\nuse futures::select;\r\nuse parking_lot::Mutex;\r\nus"
},
{
"path": "src/crusader-lib/src/lib.rs",
"chars": 1030,
"preview": "#![allow(\n clippy::new_without_default,\n clippy::too_many_arguments,\n clippy::useless_format,\n clippy::type_"
},
{
"path": "src/crusader-lib/src/peer.rs",
"chars": 11016,
"preview": "use crate::common::{connect, LatencyResult};\r\n#[cfg(feature = \"client\")]\r\nuse crate::common::{Config, Msg};\r\n#[cfg(featu"
},
{
"path": "src/crusader-lib/src/plot.rs",
"chars": 54052,
"preview": "use anyhow::{anyhow, Context};\r\nuse image::{ImageBuffer, ImageFormat, Rgb};\r\nuse plotters::coord::types::RangedCoordf64;"
},
{
"path": "src/crusader-lib/src/protocol.rs",
"chars": 3842,
"preview": "use anyhow::Context;\r\nuse bytes::{Bytes, BytesMut};\r\nuse futures::{Sink, SinkExt, Stream, StreamExt};\r\nuse serde::{Deser"
},
{
"path": "src/crusader-lib/src/remote.html",
"chars": 8479,
"preview": "<!doctype html>\r\n<html>\r\n\r\n<head>\r\n <meta charset=\"UTF-8\">\r\n <title>Crusader Remote Client</title>\r\n</head>\r\n<styl"
},
{
"path": "src/crusader-lib/src/remote.rs",
"chars": 8876,
"preview": "use crate::common::{interface_ips, Config};\r\nuse crate::plot::save_graph_to_mem;\r\nuse crate::test::{test_async, timed, P"
},
{
"path": "src/crusader-lib/src/serve.rs",
"chars": 24817,
"preview": "use anyhow::{anyhow, bail, Context};\r\nuse futures::{pin_mut, select, FutureExt};\r\nuse parking_lot::Mutex;\r\nuse socket2::"
},
{
"path": "src/crusader-lib/src/test.rs",
"chars": 32260,
"preview": "use crate::common::{\r\n connect, data, fresh_socket_addr, hello, measure_latency, ping_recv, ping_send, read_data,\r\n "
}
]
// ... and 4 more files (download for full content)
About this extraction
This page contains the full source code of the Zoxc/crusader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 64 files (1002.4 KB), approximately 268.8k tokens, and a symbol index with 1414 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.