initial ? wtf
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Maven
|
||||||
|
/target/
|
||||||
|
**/target/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# IntelliJ IDEA
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Bench results
|
||||||
|
fpr-bench/results/
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AskMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EditMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/fpr-bench/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/fpr-bench/src/main/resources" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/fpr-core/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/fpr-core/src/main/resources" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/fpr-netty/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/fpr-netty/src/main/resources" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+14
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="MavenProjectsManager">
|
||||||
|
<option name="originalFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/pom.xml" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="corretto-11" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Relism
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# FastPathRouter (FPR)
|
||||||
|
|
||||||
|
FastPathRouter is a tiny, GC-friendly routing core designed to compile startup-time route definitions into immutable, array-based match tables. The hot path avoids String and Map allocations and works directly on byte views.
|
||||||
|
|
||||||
|
Modules:
|
||||||
|
- fpr-core: routing core and compiler
|
||||||
|
- fpr-netty: Netty adapters (ByteBuf view + path extraction)
|
||||||
|
- fpr-bench: JMH benchmarks
|
||||||
|
|
||||||
|
Philosophy:
|
||||||
|
- build routes at startup, compile into frozen tables
|
||||||
|
- match using byte-level comparisons
|
||||||
|
- avoid String/Map allocations in the hot path
|
||||||
|
|
||||||
|
Route syntax:
|
||||||
|
- params use `{name}` (e.g. `/users/{id}`)
|
||||||
|
- mixed segments are allowed (e.g. `/static/pre-{x}-suf`)
|
||||||
|
- `*` matches a single segment, `**` matches the rest of the path
|
||||||
|
|
||||||
|
Zero-copy param extraction:
|
||||||
|
```java
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/users/{id}/orders/{orderId}"), "H");
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
String[] paramNames = builder.paramNames();
|
||||||
|
|
||||||
|
ByteView view = ...;
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
router.match(view, out);
|
||||||
|
out.forEachParam(view, paramNames, (name, bytes, start, len) -> {
|
||||||
|
// Use bytes + start/len as needed; convert only if required.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and test:
|
||||||
|
```
|
||||||
|
mvn -q -DskipTests=false test
|
||||||
|
```
|
||||||
|
|
||||||
|
Run benchmarks:
|
||||||
|
```
|
||||||
|
mvn -pl fpr-bench -DskipTests package
|
||||||
|
java -jar fpr-bench/target/fpr-bench-1.0-SNAPSHOT-shaded.jar -wi 5 -i 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the Maven exec profile:
|
||||||
|
```
|
||||||
|
mvn -pl fpr-bench -am -Pbench verify
|
||||||
|
```
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
FastPathRouter is split into a small public API and an internal compiler/matcher pipeline.
|
||||||
|
|
||||||
|
1) Builder
|
||||||
|
Routes are added to `RouterBuilder` using structured `RoutePattern` segments. The builder assigns parameter key ids and collects handler bindings.
|
||||||
|
|
||||||
|
2) Compile / freeze
|
||||||
|
Routes are compiled into a graph and then frozen into immutable arrays. The compile pipeline lives in `dev.relism.fpr.core.internal.compile`:
|
||||||
|
- `RouteCompiler` orchestrates the process.
|
||||||
|
- `RouteGraph` is the intermediate state/edge graph.
|
||||||
|
- `FreezeWriter` emits primitive arrays and the blob.
|
||||||
|
- `IndexBuilder` builds per-state first-byte indexes.
|
||||||
|
|
||||||
|
3) Match
|
||||||
|
Matching walks the input path by segments, compares bytes, and records param spans in a reusable `MatchResult` container. Runtime pieces live in `dev.relism.fpr.core.internal.runtime`:
|
||||||
|
- `FrozenRouter` is the match loop and state transitions.
|
||||||
|
- `EdgeDispatch` selects candidate edges for a state.
|
||||||
|
- `SegmentMatcher` evaluates a single segment (literal/mixed/param/wild).
|
||||||
|
- `ByteCompare` performs bulk byte comparisons.
|
||||||
|
|
||||||
|
No Strings or Maps are created during matching.
|
||||||
|
If you need params as name/value pairs, `MatchResult.forEachParam` can stream spans via `ParamConsumer` without allocations.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fastpathrouter-parent</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>fpr-bench</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-bom</artifactId>
|
||||||
|
<version>${netty.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fpr-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fpr-netty</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-buffer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>bench</id>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>run-bench</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>exec</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<executable>java</executable>
|
||||||
|
<arguments>
|
||||||
|
<argument>-jar</argument>
|
||||||
|
<argument>${project.build.directory}/${project.build.finalName}-shaded.jar</argument>
|
||||||
|
<argument>-wi</argument>
|
||||||
|
<argument>1</argument>
|
||||||
|
<argument>-i</argument>
|
||||||
|
<argument>1</argument>
|
||||||
|
<argument>-f</argument>
|
||||||
|
<argument>1</argument>
|
||||||
|
</arguments>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package dev.relism.fpr.bench;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
|
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||||
|
import org.openjdk.jmh.annotations.Mode;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
|
||||||
|
@BenchmarkMode({Mode.Throughput, Mode.SampleTime})
|
||||||
|
public class RouterBench {
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayLiteral(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayLiteral(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayParam(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayParam(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayMixed(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayMixed(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayCatchAll(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayCatchAll(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyLiteral(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyLiteral(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyParam(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyParam(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyMixed(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyMixed(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyCatchAll(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyCatchAll(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchLiteralHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchLiteralHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchParamHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchParamHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchMixedHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchMixedHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchCatchAllHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchCatchAllHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchLargeRouteSet(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchLargeRouteSet(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchVariety(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchVariety(blackhole);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package dev.relism.fpr.bench;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
|
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||||
|
import org.openjdk.jmh.annotations.Mode;
|
||||||
|
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@BenchmarkMode(Mode.SampleTime)
|
||||||
|
@OutputTimeUnit(TimeUnit.MICROSECONDS)
|
||||||
|
public class RouterBenchLatency {
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayLiteral(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayLiteral(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayParam(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayParam(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayMixed(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayMixed(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayCatchAll(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayCatchAll(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyLiteral(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyLiteral(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyParam(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyParam(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyMixed(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyMixed(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyCatchAll(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyCatchAll(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchLiteralHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchLiteralHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchParamHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchParamHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchMixedHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchMixedHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchCatchAllHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchCatchAllHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchLargeRouteSet(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchLargeRouteSet(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchVariety(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchVariety(blackhole);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package dev.relism.fpr.bench;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.fpr.core.FastPathRouter;
|
||||||
|
import dev.relism.fpr.core.MatchResult;
|
||||||
|
import dev.relism.fpr.core.RouterBuilder;
|
||||||
|
import dev.relism.fpr.core.dsl.StringRouteParser;
|
||||||
|
import dev.relism.fpr.netty.NettyByteBufView;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import org.openjdk.jmh.annotations.Level;
|
||||||
|
import org.openjdk.jmh.annotations.Scope;
|
||||||
|
import org.openjdk.jmh.annotations.Setup;
|
||||||
|
import org.openjdk.jmh.annotations.State;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.VarHandle;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public class RouterBenchState {
|
||||||
|
private FastPathRouter<ByteView, String> routerSmall;
|
||||||
|
private FastPathRouter<ByteView, String> routerLiteralHeavy;
|
||||||
|
private FastPathRouter<ByteView, String> routerParamHeavy;
|
||||||
|
private FastPathRouter<ByteView, String> routerMixedHeavy;
|
||||||
|
private FastPathRouter<ByteView, String> routerCatchAll;
|
||||||
|
private FastPathRouter<ByteView, String> routerLarge;
|
||||||
|
|
||||||
|
private MatchResult<String> outSmall;
|
||||||
|
private MatchResult<String> outLarge;
|
||||||
|
|
||||||
|
private ByteView arrayLiteral;
|
||||||
|
private ByteView arrayParam;
|
||||||
|
private ByteView arrayMixed;
|
||||||
|
private ByteView arrayCatchAll;
|
||||||
|
private NettyByteBufView nettyLiteral;
|
||||||
|
private NettyByteBufView nettyParam;
|
||||||
|
private NettyByteBufView nettyMixed;
|
||||||
|
private NettyByteBufView nettyCatchAll;
|
||||||
|
|
||||||
|
private ByteView literalHeavyPath;
|
||||||
|
private ByteView paramHeavyPath;
|
||||||
|
private ByteView mixedHeavyPath;
|
||||||
|
private ByteView catchAllPath;
|
||||||
|
private ByteView largePath;
|
||||||
|
|
||||||
|
private ByteView[] varietyPaths;
|
||||||
|
private int varietyIndex;
|
||||||
|
|
||||||
|
private ByteBuf nettyBufLiteral;
|
||||||
|
private ByteBuf nettyBufParam;
|
||||||
|
private ByteBuf nettyBufMixed;
|
||||||
|
private ByteBuf nettyBufCatchAll;
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void setup() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/users"), "A");
|
||||||
|
builder.add(StringRouteParser.parse("/users/{id}"), "B");
|
||||||
|
builder.add(StringRouteParser.parse("/users/{id}/orders/{orderId}"), "C");
|
||||||
|
builder.add(StringRouteParser.parse("/assets/**"), "D");
|
||||||
|
builder.add(StringRouteParser.parse("/static/pre-{x}-suf"), "E");
|
||||||
|
routerSmall = builder.compile();
|
||||||
|
outSmall = new MatchResult<>(builder.maxParamCount(), 64);
|
||||||
|
|
||||||
|
byte[] literal = "/users".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
byte[] param = "/users/123".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
byte[] mixed = "/static/pre-xyz-suf".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
byte[] catchAll = "/assets/css/app.css".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
|
||||||
|
arrayLiteral = new ByteArrayView(literal);
|
||||||
|
arrayParam = new ByteArrayView(param);
|
||||||
|
arrayMixed = new ByteArrayView(mixed);
|
||||||
|
arrayCatchAll = new ByteArrayView(catchAll);
|
||||||
|
|
||||||
|
nettyBufLiteral = Unpooled.wrappedBuffer(literal);
|
||||||
|
nettyBufParam = Unpooled.wrappedBuffer(param);
|
||||||
|
nettyBufMixed = Unpooled.wrappedBuffer(mixed);
|
||||||
|
nettyBufCatchAll = Unpooled.wrappedBuffer(catchAll);
|
||||||
|
|
||||||
|
nettyLiteral = new NettyByteBufView(nettyBufLiteral, 0, nettyBufLiteral.readableBytes());
|
||||||
|
nettyParam = new NettyByteBufView(nettyBufParam, 0, nettyBufParam.readableBytes());
|
||||||
|
nettyMixed = new NettyByteBufView(nettyBufMixed, 0, nettyBufMixed.readableBytes());
|
||||||
|
nettyCatchAll = new NettyByteBufView(nettyBufCatchAll, 0, nettyBufCatchAll.readableBytes());
|
||||||
|
|
||||||
|
routerLiteralHeavy = buildLiteralHeavy();
|
||||||
|
routerParamHeavy = buildParamHeavy();
|
||||||
|
routerMixedHeavy = buildMixedHeavy();
|
||||||
|
routerCatchAll = buildCatchAll();
|
||||||
|
routerLarge = buildLarge();
|
||||||
|
outLarge = new MatchResult<>(4, 128);
|
||||||
|
|
||||||
|
literalHeavyPath = new ByteArrayView("/route-199".getBytes(StandardCharsets.US_ASCII));
|
||||||
|
paramHeavyPath = new ByteArrayView("/p199/alpha".getBytes(StandardCharsets.US_ASCII));
|
||||||
|
mixedHeavyPath = new ByteArrayView("/m199/pre-xyz-suf".getBytes(StandardCharsets.US_ASCII));
|
||||||
|
catchAllPath = new ByteArrayView("/assets/dir/file.js".getBytes(StandardCharsets.US_ASCII));
|
||||||
|
largePath = new ByteArrayView("/r9999".getBytes(StandardCharsets.US_ASCII));
|
||||||
|
|
||||||
|
varietyPaths = new ByteView[]{
|
||||||
|
new ByteArrayView("/users".getBytes(StandardCharsets.US_ASCII)),
|
||||||
|
new ByteArrayView("/users/7".getBytes(StandardCharsets.US_ASCII)),
|
||||||
|
new ByteArrayView("/static/pre-foo-suf".getBytes(StandardCharsets.US_ASCII)),
|
||||||
|
new ByteArrayView("/assets/img/logo.png".getBytes(StandardCharsets.US_ASCII)),
|
||||||
|
new ByteArrayView("/users/9/orders/3".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchArrayLiteral(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerSmall.match(arrayLiteral, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchArrayParam(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerSmall.match(arrayParam, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchArrayMixed(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerSmall.match(arrayMixed, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchArrayCatchAll(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerSmall.match(arrayCatchAll, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchNettyLiteral(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerSmall.match(nettyLiteral, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchNettyParam(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerSmall.match(nettyParam, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchNettyMixed(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerSmall.match(nettyMixed, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchNettyCatchAll(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerSmall.match(nettyCatchAll, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchLiteralHeavy(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerLiteralHeavy.match(literalHeavyPath, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchParamHeavy(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerParamHeavy.match(paramHeavyPath, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchMixedHeavy(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerMixedHeavy.match(mixedHeavyPath, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchCatchAllHeavy(Blackhole blackhole) {
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerCatchAll.match(catchAllPath, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchLargeRouteSet(Blackhole blackhole) {
|
||||||
|
outLarge.reset();
|
||||||
|
int id = routerLarge.match(largePath, outLarge);
|
||||||
|
blackhole.consume(outLarge.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int matchVariety(Blackhole blackhole) {
|
||||||
|
if (varietyIndex >= varietyPaths.length) {
|
||||||
|
varietyIndex = 0;
|
||||||
|
}
|
||||||
|
ByteView view = varietyPaths[varietyIndex++];
|
||||||
|
outSmall.reset();
|
||||||
|
int id = routerSmall.match(view, outSmall);
|
||||||
|
blackhole.consume(outSmall.handler());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FastPathRouter<ByteView, String> buildLiteralHeavy() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
builder.add(StringRouteParser.parse("/route-" + i), "L" + i);
|
||||||
|
}
|
||||||
|
return builder.compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FastPathRouter<ByteView, String> buildParamHeavy() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
builder.add(StringRouteParser.parse("/p" + i + "/{id}"), "P" + i);
|
||||||
|
}
|
||||||
|
return builder.compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FastPathRouter<ByteView, String> buildMixedHeavy() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
builder.add(StringRouteParser.parse("/m" + i + "/pre-{x}-suf"), "M" + i);
|
||||||
|
}
|
||||||
|
return builder.compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FastPathRouter<ByteView, String> buildCatchAll() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/assets/**"), "CATCH");
|
||||||
|
builder.add(StringRouteParser.parse("/assets/images/**"), "CATCH2");
|
||||||
|
return builder.compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FastPathRouter<ByteView, String> buildLarge() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
for (int i = 0; i < 10_000; i++) {
|
||||||
|
builder.add(StringRouteParser.parse("/r" + i), "R" + i);
|
||||||
|
}
|
||||||
|
return builder.compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ByteArrayView implements ByteView {
|
||||||
|
private static final VarHandle LONG_VIEW = MethodHandles.byteArrayViewVarHandle(long[].class, java.nio.ByteOrder.LITTLE_ENDIAN);
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
private ByteArrayView(byte[] bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int length() {
|
||||||
|
return bytes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte byteAt(int index) {
|
||||||
|
return bytes[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsLong() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long longAt(int index) {
|
||||||
|
return (long) LONG_VIEW.get(bytes, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package dev.relism.fpr.bench;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
|
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||||
|
import org.openjdk.jmh.annotations.Mode;
|
||||||
|
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@BenchmarkMode(Mode.Throughput)
|
||||||
|
@OutputTimeUnit(TimeUnit.SECONDS)
|
||||||
|
public class RouterBenchThroughput {
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayLiteral(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayLiteral(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayParam(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayParam(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayMixed(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayMixed(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchArrayCatchAll(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchArrayCatchAll(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyLiteral(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyLiteral(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyParam(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyParam(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyMixed(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyMixed(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchNettyCatchAll(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchNettyCatchAll(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchLiteralHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchLiteralHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchParamHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchParamHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchMixedHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchMixedHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchCatchAllHeavy(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchCatchAllHeavy(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchLargeRouteSet(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchLargeRouteSet(blackhole);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public int matchVariety(RouterBenchState state, Blackhole blackhole) {
|
||||||
|
return state.matchVariety(blackhole);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# FPR Bench CLI
|
||||||
|
|
||||||
|
Run JMH benchmarks via a single Python CLI and export JSON results.
|
||||||
|
|
||||||
|
Requires Python 3.4+.
|
||||||
|
|
||||||
|
## Run a benchmark
|
||||||
|
From the repo root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python fpr-bench/tools/fpr_bench.py run
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds the shaded JMH jar and writes results to `fpr-bench/results/`.
|
||||||
|
You will be prompted for benchmark types and common JMH settings.
|
||||||
|
|
||||||
|
Available types: `throughput`, `latency`, `common` (runs the combined bench).
|
||||||
|
|
||||||
|
## Pass JMH arguments
|
||||||
|
Use `--` to pass flags directly to JMH (this skips the menu):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python fpr-bench/tools/fpr_bench.py run -- --wi 5 -i 5 -f 5 -tu us
|
||||||
|
```
|
||||||
|
|
||||||
|
## Results location
|
||||||
|
JSON output is stored in:
|
||||||
|
|
||||||
|
```
|
||||||
|
fpr-bench/results/YYYYMMDD_HHMMSS__<tag>__<type>.json
|
||||||
|
```
|
||||||
|
|
||||||
|
If multiple types are selected, one JSON file is produced per type.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
Skip the build and use a custom tag:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python fpr-bench/tools/fpr_bench.py run --no-build --tag smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
Run only latency and throughput types non-interactively:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python fpr-bench/tools/fpr_bench.py run --types latency,throughput -- --wi 3 -i 3 -f 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Use custom `mvn` and `java` executables:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python fpr-bench/tools/fpr_bench.py run --mvn mvn.cmd --java java
|
||||||
|
```
|
||||||
|
|
||||||
|
If another JMH instance is detected, the tool will try to terminate it. If that
|
||||||
|
fails, it runs with `-Djmh.ignoreLock=true`.
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
except ImportError:
|
||||||
|
print("Error: Python 3.4+ is required to run this tool.", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
BENCH_TYPES = {
|
||||||
|
"throughput": "dev.relism.fpr.bench.RouterBenchThroughput",
|
||||||
|
"latency": "dev.relism.fpr.bench.RouterBenchLatency",
|
||||||
|
"common": "dev.relism.fpr.bench.RouterBench",
|
||||||
|
}
|
||||||
|
BENCH_ORDER = ["throughput", "latency", "common"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Run FPR JMH benchmarks and export JSON results."
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest="command")
|
||||||
|
|
||||||
|
run_parser = subparsers.add_parser("run", help="Build and run JMH benchmarks.")
|
||||||
|
run_parser.add_argument("--tag", default="default", help="Tag for the output run folder.")
|
||||||
|
run_parser.add_argument(
|
||||||
|
"--no-build",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip the Maven build step.",
|
||||||
|
)
|
||||||
|
run_parser.add_argument("--mvn", default="mvn", help="Maven executable path.")
|
||||||
|
run_parser.add_argument("--java", default="java", help="Java executable path.")
|
||||||
|
run_parser.add_argument("--jar", help="Path to the shaded JMH jar.")
|
||||||
|
run_parser.add_argument(
|
||||||
|
"--types",
|
||||||
|
help="Comma-separated benchmark types: throughput, latency, common.",
|
||||||
|
)
|
||||||
|
run_parser.add_argument(
|
||||||
|
"--prof-gc",
|
||||||
|
action="store_true",
|
||||||
|
help="Enable JMH GC profiler (-prof gc).",
|
||||||
|
)
|
||||||
|
run_parser.add_argument(
|
||||||
|
"jmh_args",
|
||||||
|
nargs=argparse.REMAINDER,
|
||||||
|
help="Arguments passed to JMH (use -- to separate).",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
if args.command is None:
|
||||||
|
parser.print_help()
|
||||||
|
return None
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root():
|
||||||
|
# assumes this script lives under fpr-bench/tools (or similar)
|
||||||
|
return Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_tag(tag):
|
||||||
|
tag = tag.strip() if tag else ""
|
||||||
|
if not tag:
|
||||||
|
return "default"
|
||||||
|
invalid_chars = set('<>:"/\\|?*')
|
||||||
|
if any(ch in invalid_chars for ch in tag):
|
||||||
|
raise ValueError('Tag contains invalid filename characters: <>:"/\\|?*')
|
||||||
|
return tag
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_executable(executable, label):
|
||||||
|
resolved = shutil.which(executable)
|
||||||
|
if resolved:
|
||||||
|
return resolved
|
||||||
|
if Path(executable).is_file():
|
||||||
|
return str(Path(executable))
|
||||||
|
raise RuntimeError("{} executable not found: {}".format(label, executable))
|
||||||
|
|
||||||
|
|
||||||
|
def run_subprocess(cmd, cwd, failure_message):
|
||||||
|
try:
|
||||||
|
return_code = subprocess.call(cmd, cwd=cwd)
|
||||||
|
except OSError as exc:
|
||||||
|
print("{}: {}".format(failure_message, exc), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
if return_code != 0:
|
||||||
|
print(
|
||||||
|
"{} (exit code {})".format(failure_message, return_code),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return return_code
|
||||||
|
|
||||||
|
|
||||||
|
def run_subprocess_capture(cmd, cwd, log_path, failure_message):
|
||||||
|
"""
|
||||||
|
Runs a subprocess, captures combined stdout+stderr, and always writes it to log_path.
|
||||||
|
Returns the process exit code (or 2 on OS errors).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
cwd=cwd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
except OSError as exc:
|
||||||
|
try:
|
||||||
|
log_path.write_text(str(exc) + "\n", encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
print("{}: {}".format(failure_message, exc), file=sys.stderr)
|
||||||
|
print("Log: {}".format(log_path), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
try:
|
||||||
|
with log_path.open("wb") as log_file:
|
||||||
|
stream = proc.stdout
|
||||||
|
if stream is None:
|
||||||
|
proc.wait()
|
||||||
|
else:
|
||||||
|
for chunk in iter(lambda: stream.read(4096), b""):
|
||||||
|
log_file.write(chunk)
|
||||||
|
try:
|
||||||
|
sys.stdout.buffer.write(chunk)
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
except AttributeError:
|
||||||
|
sys.stdout.write(chunk.decode("utf-8", errors="replace"))
|
||||||
|
sys.stdout.flush()
|
||||||
|
return_code = proc.wait()
|
||||||
|
except OSError as exc:
|
||||||
|
print("Warning: could not write log {}: {}".format(log_path, exc), file=sys.stderr)
|
||||||
|
return_code = proc.wait()
|
||||||
|
|
||||||
|
if return_code != 0:
|
||||||
|
print("{} (exit code {})".format(failure_message, return_code), file=sys.stderr)
|
||||||
|
print("Log: {}".format(log_path), file=sys.stderr)
|
||||||
|
return return_code
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_tag(default_tag):
|
||||||
|
raw = input("Tag [{}]: ".format(default_tag)).strip()
|
||||||
|
return raw if raw else default_tag
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_int(label, default_value):
|
||||||
|
while True:
|
||||||
|
raw = input("{} [{}]: ".format(label, default_value)).strip()
|
||||||
|
if not raw:
|
||||||
|
return default_value
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except ValueError:
|
||||||
|
print("Please enter a whole number.")
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_optional(label):
|
||||||
|
raw = input("{} (blank to skip): ".format(label)).strip()
|
||||||
|
return raw if raw else None
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_bench_types():
|
||||||
|
while True:
|
||||||
|
print("Select benchmark types:")
|
||||||
|
for idx, name in enumerate(BENCH_ORDER, start=1):
|
||||||
|
print(" {}) {}".format(idx, name))
|
||||||
|
print(" a) all")
|
||||||
|
raw = input("Choice [a]: ").strip().lower()
|
||||||
|
if raw in ("", "a", "all"):
|
||||||
|
return list(BENCH_ORDER)
|
||||||
|
|
||||||
|
tokens = [token for token in raw.replace(",", " ").split() if token]
|
||||||
|
selected = []
|
||||||
|
invalid = None
|
||||||
|
for token in tokens:
|
||||||
|
if token.isdigit():
|
||||||
|
index = int(token)
|
||||||
|
if 1 <= index <= len(BENCH_ORDER):
|
||||||
|
name = BENCH_ORDER[index - 1]
|
||||||
|
if name not in selected:
|
||||||
|
selected.append(name)
|
||||||
|
continue
|
||||||
|
if token in BENCH_TYPES:
|
||||||
|
if token not in selected:
|
||||||
|
selected.append(token)
|
||||||
|
continue
|
||||||
|
invalid = token
|
||||||
|
break
|
||||||
|
|
||||||
|
if selected and invalid is None:
|
||||||
|
return selected
|
||||||
|
if invalid:
|
||||||
|
print("Unknown selection: {}".format(invalid))
|
||||||
|
else:
|
||||||
|
print("No valid selections provided.")
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_jmh_args():
|
||||||
|
jmh_args = []
|
||||||
|
wi = prompt_int("Warmup iterations (-wi)", 5)
|
||||||
|
i = prompt_int("Measurement iterations (-i)", 5)
|
||||||
|
f = prompt_int("Forks (-f)", 1)
|
||||||
|
w = prompt_optional("Warmup time (-w, e.g. 1s)")
|
||||||
|
r = prompt_optional("Measurement time (-r, e.g. 1s)")
|
||||||
|
t = prompt_optional("Threads (-t)")
|
||||||
|
tu = prompt_optional("Time unit (-tu, e.g. us)")
|
||||||
|
|
||||||
|
if wi is not None:
|
||||||
|
jmh_args.extend(["-wi", str(wi)])
|
||||||
|
if i is not None:
|
||||||
|
jmh_args.extend(["-i", str(i)])
|
||||||
|
if f is not None:
|
||||||
|
jmh_args.extend(["-f", str(f)])
|
||||||
|
if w:
|
||||||
|
jmh_args.extend(["-w", w])
|
||||||
|
if r:
|
||||||
|
jmh_args.extend(["-r", r])
|
||||||
|
if t:
|
||||||
|
jmh_args.extend(["-t", t])
|
||||||
|
if tu:
|
||||||
|
jmh_args.extend(["-tu", tu])
|
||||||
|
return jmh_args
|
||||||
|
|
||||||
|
|
||||||
|
def parse_types_arg(value):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
tokens = [token.strip().lower() for token in value.split(",") if token.strip()]
|
||||||
|
if not tokens:
|
||||||
|
return None
|
||||||
|
if "all" in tokens:
|
||||||
|
return list(BENCH_ORDER)
|
||||||
|
selected = []
|
||||||
|
for token in tokens:
|
||||||
|
if token not in BENCH_TYPES:
|
||||||
|
raise ValueError("Unknown benchmark type: {}".format(token))
|
||||||
|
if token not in selected:
|
||||||
|
selected.append(token)
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def find_jmh_pids_windows():
|
||||||
|
command = [
|
||||||
|
"powershell",
|
||||||
|
"-NoProfile",
|
||||||
|
"-Command",
|
||||||
|
(
|
||||||
|
"Get-CimInstance Win32_Process | "
|
||||||
|
"Where-Object { $_.Name -match 'java' -and $_.CommandLine -and "
|
||||||
|
"($_.CommandLine -match 'org\\.openjdk\\.jmh' -or "
|
||||||
|
"$_.CommandLine -match 'jmh' -or $_.CommandLine -match 'fpr-bench') } | "
|
||||||
|
"Select-Object -ExpandProperty ProcessId"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(command, universal_newlines=True)
|
||||||
|
except (OSError, subprocess.CalledProcessError):
|
||||||
|
return None
|
||||||
|
pids = []
|
||||||
|
for line in output.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
pids.append(int(line))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return pids
|
||||||
|
|
||||||
|
|
||||||
|
def find_jmh_pids_unix():
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
["ps", "-ax", "-o", "pid=,command="], universal_newlines=True
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.CalledProcessError):
|
||||||
|
return None
|
||||||
|
pids = []
|
||||||
|
for line in output.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(None, 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
continue
|
||||||
|
pid_str, cmdline = parts
|
||||||
|
cmd_lower = cmdline.lower()
|
||||||
|
if "java" not in cmd_lower:
|
||||||
|
continue
|
||||||
|
if "org.openjdk.jmh" in cmd_lower or "jmh" in cmd_lower or "fpr-bench" in cmd_lower:
|
||||||
|
try:
|
||||||
|
pids.append(int(pid_str))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return pids
|
||||||
|
|
||||||
|
|
||||||
|
def kill_jmh_processes():
|
||||||
|
if os.name == "nt":
|
||||||
|
pids = find_jmh_pids_windows()
|
||||||
|
else:
|
||||||
|
pids = find_jmh_pids_unix()
|
||||||
|
|
||||||
|
if pids is None:
|
||||||
|
return False
|
||||||
|
if not pids:
|
||||||
|
return True
|
||||||
|
|
||||||
|
success = True
|
||||||
|
if os.name == "nt":
|
||||||
|
for pid in pids:
|
||||||
|
return_code = subprocess.call(
|
||||||
|
["taskkill", "/PID", str(pid), "/T", "/F"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
if return_code != 0:
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
for pid in pids:
|
||||||
|
return_code = subprocess.call(
|
||||||
|
["kill", "-9", str(pid)],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
if return_code != 0:
|
||||||
|
success = False
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
def should_ignore_lock(kill_success):
|
||||||
|
if not kill_success:
|
||||||
|
return True
|
||||||
|
lock_path = Path(tempfile.gettempdir()) / "jmh.lock"
|
||||||
|
return lock_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def find_shaded_jar(bench_dir, override_path):
|
||||||
|
if override_path:
|
||||||
|
jar_path = Path(override_path).expanduser()
|
||||||
|
if not jar_path.is_absolute():
|
||||||
|
jar_path = (Path.cwd() / jar_path).resolve()
|
||||||
|
if not jar_path.is_file():
|
||||||
|
raise RuntimeError("Jar not found: {}".format(jar_path))
|
||||||
|
return jar_path
|
||||||
|
|
||||||
|
target_dir = bench_dir / "target"
|
||||||
|
if not target_dir.exists():
|
||||||
|
raise RuntimeError("Jar not found: {} does not exist".format(target_dir))
|
||||||
|
candidates = sorted(
|
||||||
|
target_dir.glob("*shaded*.jar"),
|
||||||
|
key=lambda path: path.stat().st_mtime,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError("Jar not found: no shaded JMH jar under {}".format(target_dir))
|
||||||
|
return candidates[0].resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_results_dir(results_dir):
|
||||||
|
if results_dir.exists() and not results_dir.is_dir():
|
||||||
|
raise RuntimeError("Results path exists but is not a directory: {}".format(results_dir))
|
||||||
|
results_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_run_dir(results_dir, timestamp, tag):
|
||||||
|
safe_tag = validate_tag(tag)
|
||||||
|
folder_name = "{}__{}".format(timestamp, safe_tag)
|
||||||
|
run_dir = results_dir / folder_name
|
||||||
|
run_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
return run_dir.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def output_paths_for(run_dir, bench_type, prof_gc):
|
||||||
|
suffix = "__gc" if prof_gc else ""
|
||||||
|
json_path = run_dir / "{}{}.json".format(bench_type, suffix)
|
||||||
|
log_path = run_dir / "{}{}.log".format(bench_type, suffix)
|
||||||
|
return json_path.resolve(), log_path.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def run_benchmarks(args):
|
||||||
|
root = repo_root()
|
||||||
|
bench_dir = root / "fpr-bench"
|
||||||
|
results_dir = bench_dir / "results"
|
||||||
|
try:
|
||||||
|
ensure_results_dir(results_dir)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print("Error: {}".format(exc), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
jmh_args = list(args.jmh_args or [])
|
||||||
|
if jmh_args and jmh_args[0] == "--":
|
||||||
|
jmh_args = jmh_args[1:]
|
||||||
|
|
||||||
|
interactive = not jmh_args and sys.stdin.isatty()
|
||||||
|
if interactive:
|
||||||
|
tag = prompt_tag(args.tag or "default")
|
||||||
|
else:
|
||||||
|
tag = args.tag
|
||||||
|
|
||||||
|
try:
|
||||||
|
tag = validate_tag(tag)
|
||||||
|
except ValueError as exc:
|
||||||
|
print("Error: {}".format(exc), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
try:
|
||||||
|
selected_types = parse_types_arg(args.types)
|
||||||
|
except ValueError as exc:
|
||||||
|
print("Error: {}".format(exc), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
if selected_types is None:
|
||||||
|
if interactive:
|
||||||
|
selected_types = prompt_bench_types()
|
||||||
|
else:
|
||||||
|
selected_types = list(BENCH_ORDER)
|
||||||
|
|
||||||
|
if interactive:
|
||||||
|
jmh_args = prompt_jmh_args()
|
||||||
|
|
||||||
|
if not args.no_build:
|
||||||
|
try:
|
||||||
|
mvn_exec = resolve_executable(args.mvn, "Maven")
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print("Error: {}".format(exc), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
mvn_cmd = [mvn_exec, "-q", "-pl", "fpr-bench", "-am", "package", "-Dfile.encoding=UTF-8"]
|
||||||
|
exit_code = run_subprocess(mvn_cmd, root, "Maven build failed")
|
||||||
|
if exit_code != 0:
|
||||||
|
return exit_code
|
||||||
|
|
||||||
|
try:
|
||||||
|
jar_path = find_shaded_jar(bench_dir, args.jar)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print("Error: {}".format(exc), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
ignore_lock = should_ignore_lock(kill_jmh_processes())
|
||||||
|
if ignore_lock:
|
||||||
|
print(
|
||||||
|
"Warning: Unable to terminate existing JMH instance or lock file exists; using -Djmh.ignoreLock=true",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
java_exec = resolve_executable(args.java, "Java")
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print("Error: {}".format(exc), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
run_dir = prepare_run_dir(results_dir, timestamp, tag)
|
||||||
|
print("Run directory: {}".format(run_dir))
|
||||||
|
|
||||||
|
prof_gc = bool(getattr(args, "prof_gc", False))
|
||||||
|
|
||||||
|
for bench_type in selected_types:
|
||||||
|
bench_class = BENCH_TYPES[bench_type]
|
||||||
|
json_path, log_path = output_paths_for(run_dir, bench_type, prof_gc)
|
||||||
|
|
||||||
|
# overwrite if exists
|
||||||
|
try:
|
||||||
|
if json_path.exists():
|
||||||
|
json_path.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if log_path.exists():
|
||||||
|
log_path.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
jmh_cmd = [java_exec]
|
||||||
|
if ignore_lock:
|
||||||
|
jmh_cmd.append("-Djmh.ignoreLock=true")
|
||||||
|
jmh_cmd.extend(["-jar", str(jar_path)])
|
||||||
|
|
||||||
|
if prof_gc:
|
||||||
|
jmh_cmd.extend(["-prof", "gc"])
|
||||||
|
|
||||||
|
if jmh_args:
|
||||||
|
jmh_cmd.extend(jmh_args)
|
||||||
|
|
||||||
|
jmh_cmd.append("^{}\\.".format(bench_class))
|
||||||
|
jmh_cmd.extend(["-rf", "json", "-rff", str(json_path)])
|
||||||
|
|
||||||
|
exit_code = run_subprocess_capture(
|
||||||
|
jmh_cmd, root, log_path, "JMH run failed ({})".format(bench_type)
|
||||||
|
)
|
||||||
|
if exit_code != 0:
|
||||||
|
return exit_code
|
||||||
|
|
||||||
|
# validate JSON
|
||||||
|
try:
|
||||||
|
with json_path.open("r", encoding="utf-8") as handle:
|
||||||
|
data = json.load(handle)
|
||||||
|
if not isinstance(data, list):
|
||||||
|
raise ValueError("JSON root is not an array")
|
||||||
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||||
|
print("Error: JSON could not be parsed: {}".format(exc), file=sys.stderr)
|
||||||
|
print("JSON: {}".format(json_path), file=sys.stderr)
|
||||||
|
print("Log : {}".format(log_path), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
size_bytes = json_path.stat().st_size
|
||||||
|
print("JMH run completed ({})".format(bench_type))
|
||||||
|
print("JSON: {}".format(json_path))
|
||||||
|
print("LOG : {}".format(log_path))
|
||||||
|
print("File size: {} bytes".format(size_bytes))
|
||||||
|
print("Benchmarks: {}".format(len(data)))
|
||||||
|
|
||||||
|
print("All selected benchmarks completed. Results folder: {}".format(run_dir))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
args = parse_args(argv)
|
||||||
|
if args is None:
|
||||||
|
return 2
|
||||||
|
if args.command == "run":
|
||||||
|
return run_benchmarks(args)
|
||||||
|
print("Unknown command: {}".format(args.command), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fastpathrouter-parent</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>fpr-core</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package dev.relism.fpr.core;
|
||||||
|
|
||||||
|
public interface ByteView {
|
||||||
|
int length();
|
||||||
|
|
||||||
|
byte byteAt(int index);
|
||||||
|
|
||||||
|
default boolean supportsLong() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default long longAt(int index) {
|
||||||
|
throw new UnsupportedOperationException("longAt not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package dev.relism.fpr.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable router interface for hot-path matching.
|
||||||
|
*/
|
||||||
|
public interface FastPathRouter<I, H> {
|
||||||
|
int NO_MATCH = -1;
|
||||||
|
|
||||||
|
int match(I input, MatchResult<H> out);
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package dev.relism.fpr.core;
|
||||||
|
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable match result container for parameter spans.
|
||||||
|
*/
|
||||||
|
public final class MatchResult<H> {
|
||||||
|
private final int[] keyIds;
|
||||||
|
private final int[] starts;
|
||||||
|
private final int[] lens;
|
||||||
|
private final int[] stackState;
|
||||||
|
private final int[] stackSegStart;
|
||||||
|
private final int[] stackSegLen;
|
||||||
|
private final int[] stackNextIdx;
|
||||||
|
private final int[] stackParamMark;
|
||||||
|
private final int[] stackEdgeIndex;
|
||||||
|
private final byte[] stackKind;
|
||||||
|
private final int[] scratchKeyIds;
|
||||||
|
private final int[] scratchStarts;
|
||||||
|
private final int[] scratchLens;
|
||||||
|
private final int[] scratchEdges;
|
||||||
|
private int stackSize;
|
||||||
|
private int paramCount;
|
||||||
|
@Setter
|
||||||
|
private H handler;
|
||||||
|
private int labelId;
|
||||||
|
|
||||||
|
public MatchResult() {
|
||||||
|
this(8, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MatchResult(int maxParams) {
|
||||||
|
this(maxParams, Math.max(32, maxParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
public MatchResult(int maxParams, int maxStack) {
|
||||||
|
if (maxParams < 0) {
|
||||||
|
throw new IllegalArgumentException("maxParams must be >= 0");
|
||||||
|
}
|
||||||
|
if (maxStack <= 0) {
|
||||||
|
throw new IllegalArgumentException("maxStack must be > 0");
|
||||||
|
}
|
||||||
|
this.keyIds = new int[maxParams];
|
||||||
|
this.starts = new int[maxParams];
|
||||||
|
this.lens = new int[maxParams];
|
||||||
|
this.stackState = new int[maxStack];
|
||||||
|
this.stackSegStart = new int[maxStack];
|
||||||
|
this.stackSegLen = new int[maxStack];
|
||||||
|
this.stackNextIdx = new int[maxStack];
|
||||||
|
this.stackParamMark = new int[maxStack];
|
||||||
|
this.stackEdgeIndex = new int[maxStack];
|
||||||
|
this.stackKind = new byte[maxStack];
|
||||||
|
this.scratchKeyIds = new int[maxParams];
|
||||||
|
this.scratchStarts = new int[maxParams];
|
||||||
|
this.scratchLens = new int[maxParams];
|
||||||
|
this.scratchEdges = new int[maxStack];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears handler and params but keeps label id.
|
||||||
|
*/
|
||||||
|
public MatchResult<H> reset() {
|
||||||
|
this.paramCount = 0;
|
||||||
|
this.stackSize = 0;
|
||||||
|
this.handler = null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MatchResult<H> labelId(int labelId) {
|
||||||
|
this.labelId = labelId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int labelId() {
|
||||||
|
return labelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int paramCount() {
|
||||||
|
return paramCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates params without allocations using a precomputed name array.
|
||||||
|
* Use {@link RouterBuilder#paramNames()} to obtain the name array.
|
||||||
|
*/
|
||||||
|
public void forEachParam(ByteView view, String[] paramNames, ParamConsumer consumer) {
|
||||||
|
if (view == null) {
|
||||||
|
throw new IllegalArgumentException("view must not be null");
|
||||||
|
}
|
||||||
|
if (paramNames == null) {
|
||||||
|
throw new IllegalArgumentException("paramNames must not be null");
|
||||||
|
}
|
||||||
|
if (consumer == null) {
|
||||||
|
throw new IllegalArgumentException("consumer must not be null");
|
||||||
|
}
|
||||||
|
for (int i = 0; i < paramCount; i++) {
|
||||||
|
int keyId = keyIds[i];
|
||||||
|
if (keyId < 0 || keyId >= paramNames.length) {
|
||||||
|
throw new IllegalArgumentException("param name missing for keyId " + keyId);
|
||||||
|
}
|
||||||
|
consumer.accept(paramNames[keyId], view, starts[i], lens[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int keyIdAt(int index) {
|
||||||
|
return keyIds[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int startAt(int index) {
|
||||||
|
return starts[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int lenAt(int index) {
|
||||||
|
return lens[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public H handler() {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int mark() {
|
||||||
|
return paramCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rollbackTo(int mark) {
|
||||||
|
paramCount = mark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetTo(int mark) {
|
||||||
|
paramCount = mark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addParam(int keyId, int start, int len) {
|
||||||
|
if (paramCount >= keyIds.length) {
|
||||||
|
throw new IllegalStateException("MatchResult capacity exceeded");
|
||||||
|
}
|
||||||
|
keyIds[paramCount] = keyId;
|
||||||
|
starts[paramCount] = start;
|
||||||
|
lens[paramCount] = len;
|
||||||
|
paramCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] stackStateArray() {
|
||||||
|
return stackState;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] stackSegStartArray() {
|
||||||
|
return stackSegStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] stackSegLenArray() {
|
||||||
|
return stackSegLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] stackNextIdxArray() {
|
||||||
|
return stackNextIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] stackParamMarkArray() {
|
||||||
|
return stackParamMark;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] stackEdgeIndexArray() {
|
||||||
|
return stackEdgeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] stackKindArray() {
|
||||||
|
return stackKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
int stackSize() {
|
||||||
|
return stackSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stackSize(int size) {
|
||||||
|
this.stackSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] keyIdsArray() {
|
||||||
|
return keyIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] startsArray() {
|
||||||
|
return starts;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] lensArray() {
|
||||||
|
return lens;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] scratchKeyIdsArray() {
|
||||||
|
return scratchKeyIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] scratchStartsArray() {
|
||||||
|
return scratchStarts;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] scratchLensArray() {
|
||||||
|
return scratchLens;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] scratchEdgesArray() {
|
||||||
|
return scratchEdges;
|
||||||
|
}
|
||||||
|
|
||||||
|
void paramCount(int count) {
|
||||||
|
this.paramCount = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package dev.relism.fpr.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal access bridge for match-time scratch storage.
|
||||||
|
*/
|
||||||
|
public final class MatchResultAccess {
|
||||||
|
private MatchResultAccess() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] stackState(MatchResult<?> result) {
|
||||||
|
return result.stackStateArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] stackSegStart(MatchResult<?> result) {
|
||||||
|
return result.stackSegStartArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] stackSegLen(MatchResult<?> result) {
|
||||||
|
return result.stackSegLenArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] stackNextIdx(MatchResult<?> result) {
|
||||||
|
return result.stackNextIdxArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] stackParamMark(MatchResult<?> result) {
|
||||||
|
return result.stackParamMarkArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] stackEdgeIndex(MatchResult<?> result) {
|
||||||
|
return result.stackEdgeIndexArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] stackKind(MatchResult<?> result) {
|
||||||
|
return result.stackKindArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] keyIds(MatchResult<?> result) {
|
||||||
|
return result.keyIdsArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] starts(MatchResult<?> result) {
|
||||||
|
return result.startsArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] lens(MatchResult<?> result) {
|
||||||
|
return result.lensArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] scratchKeyIds(MatchResult<?> result) {
|
||||||
|
return result.scratchKeyIdsArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] scratchStarts(MatchResult<?> result) {
|
||||||
|
return result.scratchStartsArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] scratchLens(MatchResult<?> result) {
|
||||||
|
return result.scratchLensArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] scratchEdges(MatchResult<?> result) {
|
||||||
|
return result.scratchEdgesArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int stackSize(MatchResult<?> result) {
|
||||||
|
return result.stackSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void stackSize(MatchResult<?> result, int size) {
|
||||||
|
result.stackSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void paramCount(MatchResult<?> result, int count) {
|
||||||
|
result.paramCount(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.relism.fpr.core;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ParamConsumer {
|
||||||
|
/**
|
||||||
|
* Receives a param name and its byte span in the matched input.
|
||||||
|
*/
|
||||||
|
void accept(String name, ByteView view, int start, int len);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package dev.relism.fpr.core;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable route pattern built from segment tokens.
|
||||||
|
*/
|
||||||
|
public final class RoutePattern {
|
||||||
|
private final List<Segment> segments;
|
||||||
|
|
||||||
|
RoutePattern(List<Segment> segments) {
|
||||||
|
this.segments = segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Segment> segments() {
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern of(Segment... segments) {
|
||||||
|
return new RoutePattern(Arrays.asList(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern fromSegments(List<Segment> segments) {
|
||||||
|
return new RoutePattern(segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Literal literal(String text) {
|
||||||
|
return new Literal(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Param param(String name) {
|
||||||
|
return new Param(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Wildcard wildcard() {
|
||||||
|
return new Wildcard();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CatchAll catchAll() {
|
||||||
|
return new CatchAll(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CatchAll catchAll(String name) {
|
||||||
|
return new CatchAll(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Mixed mixed(String[] literals, String[] params) {
|
||||||
|
return new Mixed(literals, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SegmentType {
|
||||||
|
LITERAL,
|
||||||
|
PARAM,
|
||||||
|
WILDCARD,
|
||||||
|
CATCH_ALL,
|
||||||
|
MIXED
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Segment {
|
||||||
|
SegmentType type();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Literal implements Segment {
|
||||||
|
private final String text;
|
||||||
|
|
||||||
|
public Literal(String text) {
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("literal must be non-empty");
|
||||||
|
}
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String text() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SegmentType type() {
|
||||||
|
return SegmentType.LITERAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Param implements Segment {
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
public Param(String name) {
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("param name must be non-empty");
|
||||||
|
}
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SegmentType type() {
|
||||||
|
return SegmentType.PARAM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Wildcard implements Segment {
|
||||||
|
@Override
|
||||||
|
public SegmentType type() {
|
||||||
|
return SegmentType.WILDCARD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class CatchAll implements Segment {
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
public CatchAll(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SegmentType type() {
|
||||||
|
return SegmentType.CATCH_ALL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Mixed implements Segment {
|
||||||
|
private final String[] literals;
|
||||||
|
private final String[] params;
|
||||||
|
|
||||||
|
public Mixed(String[] literals, String[] params) {
|
||||||
|
if (literals == null || params == null || literals.length != params.length + 1) {
|
||||||
|
throw new IllegalArgumentException("mixed segment requires literals=paramCount+1");
|
||||||
|
}
|
||||||
|
this.literals = literals;
|
||||||
|
this.params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] literals() {
|
||||||
|
return literals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] params() {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SegmentType type() {
|
||||||
|
return SegmentType.MIXED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package dev.relism.fpr.core;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.internal.compile.RouteCompiler;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder for creating and compiling immutable routers.
|
||||||
|
*/
|
||||||
|
public final class RouterBuilder<H> {
|
||||||
|
private final List<RouteSpec<H>> routes = new ArrayList<>();
|
||||||
|
private final LinkedHashMap<String, Integer> paramIds = new LinkedHashMap<>();
|
||||||
|
private final List<String> paramNames = new ArrayList<>();
|
||||||
|
private final LinkedHashMap<String, Integer> labelIds = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
public RouterBuilder<H> add(RoutePattern pattern, H handler) {
|
||||||
|
return add(0, pattern, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouterBuilder<H> add(String label, RoutePattern pattern, H handler) {
|
||||||
|
return add(labelId(label), pattern, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouterBuilder<H> add(Enum<?> label, RoutePattern pattern, H handler) {
|
||||||
|
return add(labelId(label), pattern, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouterBuilder<H> add(String[] labels, RoutePattern pattern, H handler) {
|
||||||
|
if (labels == null || labels.length == 0) {
|
||||||
|
return add(0, pattern, handler);
|
||||||
|
}
|
||||||
|
for (String label : labels) {
|
||||||
|
add(labelId(label), pattern, handler);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouterBuilder<H> add(Enum<?>[] labels, RoutePattern pattern, H handler) {
|
||||||
|
if (labels == null || labels.length == 0) {
|
||||||
|
return add(0, pattern, handler);
|
||||||
|
}
|
||||||
|
for (Enum<?> label : labels) {
|
||||||
|
add(labelId(label), pattern, handler);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouterBuilder<H> add(int labelId, RoutePattern pattern, H handler) {
|
||||||
|
if (pattern == null) {
|
||||||
|
throw new IllegalArgumentException("pattern must not be null");
|
||||||
|
}
|
||||||
|
if (labelId < 0) {
|
||||||
|
throw new IllegalArgumentException("labelId must be >= 0");
|
||||||
|
}
|
||||||
|
registerParams(pattern);
|
||||||
|
routes.add(new RouteSpec<>(labelId, pattern, handler, routes.size()));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int maxParamCount() {
|
||||||
|
return paramIds.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns param names by key id order for optional zero-copy extraction.
|
||||||
|
* Cache the returned array for reuse; it is stable after route registration.
|
||||||
|
*/
|
||||||
|
public String[] paramNames() {
|
||||||
|
return paramNames.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int labelId(String label) {
|
||||||
|
if (label == null || label.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Integer existing = labelIds.get(label);
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
int id = labelIds.size() + 1;
|
||||||
|
labelIds.put(label, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int labelId(Enum<?> label) {
|
||||||
|
if (label == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String key = label.getDeclaringClass().getName() + "#" + label.name();
|
||||||
|
return labelId(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FastPathRouter<ByteView, H> compile() {
|
||||||
|
return RouteCompiler.compile(routes, paramIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerParams(RoutePattern pattern) {
|
||||||
|
for (RoutePattern.Segment segment : pattern.segments()) {
|
||||||
|
if (segment instanceof RoutePattern.Param) {
|
||||||
|
paramId(((RoutePattern.Param) segment).name());
|
||||||
|
} else if (segment instanceof RoutePattern.CatchAll) {
|
||||||
|
String name = ((RoutePattern.CatchAll) segment).name();
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
paramId(name);
|
||||||
|
}
|
||||||
|
} else if (segment instanceof RoutePattern.Mixed) {
|
||||||
|
for (String name : ((RoutePattern.Mixed) segment).params()) {
|
||||||
|
paramId(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int paramId(String name) {
|
||||||
|
Integer existing = paramIds.get(name);
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
int id = paramIds.size();
|
||||||
|
paramIds.put(name, id);
|
||||||
|
paramNames.add(name);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Accessors(fluent = true)
|
||||||
|
public static final class RouteSpec<H> {
|
||||||
|
private final int labelId;
|
||||||
|
private final RoutePattern pattern;
|
||||||
|
private final H handler;
|
||||||
|
private final int order;
|
||||||
|
|
||||||
|
public RouteSpec(int labelId, RoutePattern pattern, H handler, int order) {
|
||||||
|
this.labelId = labelId;
|
||||||
|
this.pattern = pattern;
|
||||||
|
this.handler = handler;
|
||||||
|
this.order = order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package dev.relism.fpr.core.dsl;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.RoutePattern;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cold-path string parser for building RoutePattern instances.
|
||||||
|
*/
|
||||||
|
public final class StringRouteParser {
|
||||||
|
private StringRouteParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern parse(String path) {
|
||||||
|
if (path == null) {
|
||||||
|
throw new IllegalArgumentException("path must not be null");
|
||||||
|
}
|
||||||
|
String trimmed = path.trim();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("path must not be empty");
|
||||||
|
}
|
||||||
|
List<RoutePattern.Segment> segments = new ArrayList<>();
|
||||||
|
int start = 0;
|
||||||
|
int len = trimmed.length();
|
||||||
|
if (trimmed.charAt(0) == '/') {
|
||||||
|
start = 1;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
int slash = trimmed.indexOf('/', start);
|
||||||
|
if (slash == -1) {
|
||||||
|
slash = len;
|
||||||
|
}
|
||||||
|
if (slash > start) {
|
||||||
|
segments.add(parseSegment(trimmed.substring(start, slash)));
|
||||||
|
} else if (slash == len) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("empty segment in path: " + path);
|
||||||
|
}
|
||||||
|
start = slash + 1;
|
||||||
|
if (start > len) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return RoutePattern.fromSegments(segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoutePattern.Segment parseSegment(String segment) {
|
||||||
|
if (segment.equals("*")) {
|
||||||
|
return RoutePattern.wildcard();
|
||||||
|
}
|
||||||
|
if (segment.equals("**")) {
|
||||||
|
return RoutePattern.catchAll();
|
||||||
|
}
|
||||||
|
int len = segment.length();
|
||||||
|
if (len >= 2 && segment.charAt(0) == '{' && segment.charAt(len - 1) == '}'
|
||||||
|
&& segment.indexOf('{', 1) == -1 && segment.indexOf('}') == len - 1) {
|
||||||
|
String name = segment.substring(1, len - 1);
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("param segment requires name");
|
||||||
|
}
|
||||||
|
if (!isValidParamName(name)) {
|
||||||
|
throw new IllegalArgumentException("param segment has invalid name: " + segment);
|
||||||
|
}
|
||||||
|
return RoutePattern.param(name);
|
||||||
|
}
|
||||||
|
if (segment.indexOf('{') >= 0 || segment.indexOf('}') >= 0) {
|
||||||
|
if (segment.indexOf('{') < 0) {
|
||||||
|
throw new IllegalArgumentException("mixed segment has stray closing brace: " + segment);
|
||||||
|
}
|
||||||
|
return parseMixed(segment);
|
||||||
|
}
|
||||||
|
return RoutePattern.literal(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoutePattern.Segment parseMixed(String segment) {
|
||||||
|
List<String> literals = new ArrayList<>();
|
||||||
|
List<String> params = new ArrayList<>();
|
||||||
|
int i = 0;
|
||||||
|
int len = segment.length();
|
||||||
|
while (i < len) {
|
||||||
|
int open = segment.indexOf('{', i);
|
||||||
|
int close = segment.indexOf('}', i);
|
||||||
|
if (close >= 0 && (open < 0 || close < open)) {
|
||||||
|
throw new IllegalArgumentException("mixed segment has stray closing brace: " + segment);
|
||||||
|
}
|
||||||
|
if (open < 0) {
|
||||||
|
literals.add(segment.substring(i));
|
||||||
|
i = len;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
literals.add(segment.substring(i, open));
|
||||||
|
int nameStart = open + 1;
|
||||||
|
int nameEnd = segment.indexOf('}', nameStart);
|
||||||
|
if (nameEnd < 0) {
|
||||||
|
throw new IllegalArgumentException("mixed segment has unterminated param: " + segment);
|
||||||
|
}
|
||||||
|
if (nameEnd == nameStart) {
|
||||||
|
throw new IllegalArgumentException("mixed segment has empty param name: " + segment);
|
||||||
|
}
|
||||||
|
String name = segment.substring(nameStart, nameEnd);
|
||||||
|
if (!isValidParamName(name)) {
|
||||||
|
throw new IllegalArgumentException("mixed segment has invalid param name: " + segment);
|
||||||
|
}
|
||||||
|
params.add(name);
|
||||||
|
i = nameEnd + 1;
|
||||||
|
}
|
||||||
|
if (params.isEmpty()) {
|
||||||
|
return RoutePattern.literal(segment);
|
||||||
|
}
|
||||||
|
if (literals.size() == params.size()) {
|
||||||
|
literals.add("");
|
||||||
|
}
|
||||||
|
if (literals.size() != params.size() + 1) {
|
||||||
|
throw new IllegalArgumentException("mixed segment literals/params mismatch: " + segment);
|
||||||
|
}
|
||||||
|
return RoutePattern.mixed(literals.toArray(new String[0]), params.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValidParamName(String name) {
|
||||||
|
for (int i = 0; i < name.length(); i++) {
|
||||||
|
char ch = name.charAt(i);
|
||||||
|
if (!(Character.isLetterOrDigit(ch) || ch == '_')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package dev.relism.fpr.core.internal.compile;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.fpr.core.FastPathRouter;
|
||||||
|
import dev.relism.fpr.core.internal.compile.lookup.LiteralLookupPlanBuilder;
|
||||||
|
import dev.relism.fpr.core.internal.compile.lookup.MixedLookupPlanBuilder;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.EdgeKind;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.FrozenRouter;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.lookup.LiteralLookupStrategy;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class FreezeWriter {
|
||||||
|
private static final int INDEX_THRESHOLD = 12;
|
||||||
|
|
||||||
|
private FreezeWriter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static <H> FastPathRouter<ByteView, H> freeze(RouteGraph<H> graph) {
|
||||||
|
List<RouteGraph.Node<H>> nodes = graph.nodes;
|
||||||
|
List<H> handlers = graph.handlers;
|
||||||
|
|
||||||
|
int stateCount = nodes.size();
|
||||||
|
int totalEdges = 0;
|
||||||
|
int totalAccepts = 0;
|
||||||
|
for (RouteGraph.Node<H> node : nodes) {
|
||||||
|
totalEdges += node.edges.size();
|
||||||
|
totalAccepts += node.accepts.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitiveBuilders.ByteBlobBuilder blob = new PrimitiveBuilders.ByteBlobBuilder(1024);
|
||||||
|
PrimitiveBuilders.IntArrayList edgeNextState = new PrimitiveBuilders.IntArrayList(totalEdges);
|
||||||
|
PrimitiveBuilders.IntArrayList edgeLabelOff = new PrimitiveBuilders.IntArrayList(totalEdges);
|
||||||
|
PrimitiveBuilders.ShortArrayList edgeLabelLen = new PrimitiveBuilders.ShortArrayList(totalEdges);
|
||||||
|
PrimitiveBuilders.LongArrayList edgeLiteralPrefix = new PrimitiveBuilders.LongArrayList(totalEdges);
|
||||||
|
PrimitiveBuilders.ByteArrayList edgeKind = new PrimitiveBuilders.ByteArrayList(totalEdges);
|
||||||
|
PrimitiveBuilders.IntArrayList edgeMixedChunkOff = new PrimitiveBuilders.IntArrayList(totalEdges);
|
||||||
|
PrimitiveBuilders.ShortArrayList edgeMixedChunkCount = new PrimitiveBuilders.ShortArrayList(totalEdges);
|
||||||
|
PrimitiveBuilders.IntArrayList edgeMixedParamOff = new PrimitiveBuilders.IntArrayList(totalEdges);
|
||||||
|
PrimitiveBuilders.ShortArrayList edgeMixedParamCount = new PrimitiveBuilders.ShortArrayList(totalEdges);
|
||||||
|
PrimitiveBuilders.ByteArrayList edgeMixedStrategy = new PrimitiveBuilders.ByteArrayList(totalEdges);
|
||||||
|
|
||||||
|
PrimitiveBuilders.IntArrayList mixedChunkOff = new PrimitiveBuilders.IntArrayList(totalEdges * 2);
|
||||||
|
PrimitiveBuilders.ShortArrayList mixedChunkLen = new PrimitiveBuilders.ShortArrayList(totalEdges * 2);
|
||||||
|
PrimitiveBuilders.ShortArrayList mixedParamKeyId = new PrimitiveBuilders.ShortArrayList(totalEdges * 2);
|
||||||
|
|
||||||
|
PrimitiveBuilders.IntArrayList acceptHandlerId = new PrimitiveBuilders.IntArrayList(totalAccepts);
|
||||||
|
PrimitiveBuilders.IntArrayList acceptLabelId = new PrimitiveBuilders.IntArrayList(totalAccepts);
|
||||||
|
PrimitiveBuilders.IntArrayList acceptRouteId = new PrimitiveBuilders.IntArrayList(totalAccepts);
|
||||||
|
|
||||||
|
PrimitiveBuilders.IntArrayList indexStart = new PrimitiveBuilders.IntArrayList(stateCount * 4);
|
||||||
|
PrimitiveBuilders.ShortArrayList indexCount = new PrimitiveBuilders.ShortArrayList(stateCount * 4);
|
||||||
|
PrimitiveBuilders.IntArrayList indexSecondOff = new PrimitiveBuilders.IntArrayList(stateCount * 4);
|
||||||
|
PrimitiveBuilders.LongArrayList literalHashKey = new PrimitiveBuilders.LongArrayList(stateCount * 8);
|
||||||
|
PrimitiveBuilders.IntArrayList literalHashEdge = new PrimitiveBuilders.IntArrayList(stateCount * 8);
|
||||||
|
|
||||||
|
int[] stateFirstEdge = new int[stateCount];
|
||||||
|
short[] stateEdgeCount = new short[stateCount];
|
||||||
|
int[] stateLiteralStart = new int[stateCount];
|
||||||
|
short[] stateLiteralCount = new short[stateCount];
|
||||||
|
byte[] stateLiteralStrategy = new byte[stateCount];
|
||||||
|
int[] stateLiteralHashOff = new int[stateCount];
|
||||||
|
int[] stateLiteralHashMask = new int[stateCount];
|
||||||
|
int[] stateMixedStart = new int[stateCount];
|
||||||
|
short[] stateMixedCount = new short[stateCount];
|
||||||
|
short[] stateMixedPrefixCount = new short[stateCount];
|
||||||
|
int[] stateWildIndex = new int[stateCount];
|
||||||
|
int[] stateParamNext = new int[stateCount];
|
||||||
|
short[] stateParamKeyId = new short[stateCount];
|
||||||
|
int[] stateCatchAllNext = new int[stateCount];
|
||||||
|
short[] stateCatchAllKeyId = new short[stateCount];
|
||||||
|
int[] stateAcceptFirst = new int[stateCount];
|
||||||
|
short[] stateAcceptCount = new short[stateCount];
|
||||||
|
int[] stateLiteralIndexOff = new int[stateCount];
|
||||||
|
int[] stateMixedIndexOff = new int[stateCount];
|
||||||
|
|
||||||
|
Arrays.fill(stateWildIndex, -1);
|
||||||
|
Arrays.fill(stateParamNext, -1);
|
||||||
|
Arrays.fill(stateCatchAllNext, -1);
|
||||||
|
Arrays.fill(stateParamKeyId, (short) -1);
|
||||||
|
Arrays.fill(stateCatchAllKeyId, (short) -1);
|
||||||
|
Arrays.fill(stateLiteralIndexOff, -1);
|
||||||
|
Arrays.fill(stateMixedIndexOff, -1);
|
||||||
|
Arrays.fill(stateLiteralHashOff, -1);
|
||||||
|
Arrays.fill(stateLiteralHashMask, -1);
|
||||||
|
Arrays.fill(stateLiteralStrategy, LiteralLookupStrategy.LINEAR);
|
||||||
|
|
||||||
|
for (int s = 0; s < stateCount; s++) {
|
||||||
|
RouteGraph.Node<H> node = nodes.get(s);
|
||||||
|
stateFirstEdge[s] = edgeNextState.size();
|
||||||
|
|
||||||
|
List<RouteGraph.Edge> literals = node.literalEdges();
|
||||||
|
List<RouteGraph.Edge> mixed = node.mixedEdges();
|
||||||
|
RouteGraph.Edge wild = node.wildEdge();
|
||||||
|
|
||||||
|
literals.sort(RouteGraph.Edge.literalComparator());
|
||||||
|
mixed.sort(RouteGraph.Edge.mixedComparator());
|
||||||
|
|
||||||
|
int literalStart = edgeNextState.size();
|
||||||
|
for (RouteGraph.Edge edge : literals) {
|
||||||
|
int off = blob.append(edge.literal);
|
||||||
|
edgeNextState.add(edge.nextState);
|
||||||
|
edgeLabelOff.add(off);
|
||||||
|
edgeLabelLen.add((short) edge.literal.length);
|
||||||
|
edgeLiteralPrefix.add(LiteralLookupPlanBuilder.prefixKey(edge.literal));
|
||||||
|
edgeKind.add((byte) edge.kind.ordinal());
|
||||||
|
edgeMixedChunkOff.add(-1);
|
||||||
|
edgeMixedChunkCount.add((short) 0);
|
||||||
|
edgeMixedParamOff.add(-1);
|
||||||
|
edgeMixedParamCount.add((short) 0);
|
||||||
|
edgeMixedStrategy.add((byte) 0);
|
||||||
|
}
|
||||||
|
int literalCount = literals.size();
|
||||||
|
|
||||||
|
int mixedStart = edgeNextState.size();
|
||||||
|
int mixedPrefixCount = 0;
|
||||||
|
for (RouteGraph.Edge edge : mixed) {
|
||||||
|
int chunkBase = mixedChunkOff.size();
|
||||||
|
for (byte[] chunk : edge.mixed.literals) {
|
||||||
|
int off = blob.append(chunk);
|
||||||
|
mixedChunkOff.add(off);
|
||||||
|
mixedChunkLen.add((short) chunk.length);
|
||||||
|
}
|
||||||
|
int paramBase = mixedParamKeyId.size();
|
||||||
|
for (short key : edge.mixed.paramKeys) {
|
||||||
|
mixedParamKeyId.add(key);
|
||||||
|
}
|
||||||
|
int firstOff = mixedChunkOff.get(chunkBase);
|
||||||
|
short firstLen = mixedChunkLen.get(chunkBase);
|
||||||
|
|
||||||
|
edgeNextState.add(edge.nextState);
|
||||||
|
edgeLabelOff.add(firstOff);
|
||||||
|
edgeLabelLen.add(firstLen);
|
||||||
|
edgeLiteralPrefix.add(0L);
|
||||||
|
edgeKind.add((byte) edge.kind.ordinal());
|
||||||
|
edgeMixedChunkOff.add(chunkBase);
|
||||||
|
edgeMixedChunkCount.add((short) edge.mixed.literals.length);
|
||||||
|
edgeMixedParamOff.add(paramBase);
|
||||||
|
edgeMixedParamCount.add((short) edge.mixed.paramKeys.length);
|
||||||
|
edgeMixedStrategy.add(MixedLookupPlanBuilder.strategyForParamCount(edge.mixed.paramKeys.length));
|
||||||
|
|
||||||
|
if (firstLen > 0) {
|
||||||
|
mixedPrefixCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int mixedCount = mixed.size();
|
||||||
|
|
||||||
|
if (wild != null) {
|
||||||
|
stateWildIndex[s] = edgeNextState.size();
|
||||||
|
edgeNextState.add(wild.nextState);
|
||||||
|
edgeLabelOff.add(0);
|
||||||
|
edgeLabelLen.add((short) 0);
|
||||||
|
edgeLiteralPrefix.add(0L);
|
||||||
|
edgeKind.add((byte) EdgeKind.WILD.ordinal());
|
||||||
|
edgeMixedChunkOff.add(-1);
|
||||||
|
edgeMixedChunkCount.add((short) 0);
|
||||||
|
edgeMixedParamOff.add(-1);
|
||||||
|
edgeMixedParamCount.add((short) 0);
|
||||||
|
edgeMixedStrategy.add((byte) 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int edgeCount = edgeNextState.size() - stateFirstEdge[s];
|
||||||
|
stateEdgeCount[s] = (short) edgeCount;
|
||||||
|
stateLiteralStart[s] = literalStart;
|
||||||
|
stateLiteralCount[s] = (short) literalCount;
|
||||||
|
stateMixedStart[s] = mixedStart;
|
||||||
|
stateMixedCount[s] = (short) mixedCount;
|
||||||
|
stateMixedPrefixCount[s] = (short) mixedPrefixCount;
|
||||||
|
|
||||||
|
if (literalCount > 0) {
|
||||||
|
byte literalStrategy = LiteralLookupPlanBuilder.selectStrategy(literalCount);
|
||||||
|
stateLiteralStrategy[s] = literalStrategy;
|
||||||
|
if (literalStrategy == LiteralLookupStrategy.HASH) {
|
||||||
|
LiteralLookupPlanBuilder.HashPlan plan = LiteralLookupPlanBuilder.buildHash(
|
||||||
|
literalHashKey,
|
||||||
|
literalHashEdge,
|
||||||
|
edgeLiteralPrefix,
|
||||||
|
edgeLabelLen,
|
||||||
|
literalStart,
|
||||||
|
literalCount
|
||||||
|
);
|
||||||
|
stateLiteralHashOff[s] = plan.offset;
|
||||||
|
stateLiteralHashMask[s] = plan.mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mixedPrefixCount > 0 && mixedCount >= INDEX_THRESHOLD) {
|
||||||
|
stateMixedIndexOff[s] = IndexBuilder.buildIndex(indexStart, indexCount, indexSecondOff, edgeLabelOff, edgeLabelLen,
|
||||||
|
blob, mixedStart, mixedCount, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
stateParamNext[s] = node.paramNext;
|
||||||
|
stateParamKeyId[s] = node.paramKeyId;
|
||||||
|
stateCatchAllNext[s] = node.catchAllNext;
|
||||||
|
stateCatchAllKeyId[s] = node.catchAllKeyId;
|
||||||
|
|
||||||
|
stateAcceptFirst[s] = acceptHandlerId.size();
|
||||||
|
for (RouteGraph.Accept accept : node.accepts) {
|
||||||
|
acceptHandlerId.add(accept.handlerId);
|
||||||
|
acceptLabelId.add(accept.labelId);
|
||||||
|
acceptRouteId.add(accept.routeId);
|
||||||
|
}
|
||||||
|
stateAcceptCount[s] = (short) node.accepts.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
H[] handlerArray = (H[]) handlers.toArray(new Object[0]);
|
||||||
|
|
||||||
|
return new FrozenRouter<>(
|
||||||
|
blob.toArray(),
|
||||||
|
handlerArray,
|
||||||
|
stateFirstEdge,
|
||||||
|
stateEdgeCount,
|
||||||
|
stateLiteralStart,
|
||||||
|
stateLiteralCount,
|
||||||
|
stateLiteralStrategy,
|
||||||
|
stateLiteralHashOff,
|
||||||
|
stateLiteralHashMask,
|
||||||
|
stateMixedStart,
|
||||||
|
stateMixedCount,
|
||||||
|
stateMixedPrefixCount,
|
||||||
|
stateWildIndex,
|
||||||
|
stateParamNext,
|
||||||
|
stateParamKeyId,
|
||||||
|
stateCatchAllNext,
|
||||||
|
stateCatchAllKeyId,
|
||||||
|
stateAcceptFirst,
|
||||||
|
stateAcceptCount,
|
||||||
|
stateLiteralIndexOff,
|
||||||
|
stateMixedIndexOff,
|
||||||
|
edgeNextState.toArray(),
|
||||||
|
edgeLabelOff.toArray(),
|
||||||
|
edgeLabelLen.toArray(),
|
||||||
|
edgeLiteralPrefix.toArray(),
|
||||||
|
edgeKind.toArray(),
|
||||||
|
edgeMixedChunkOff.toArray(),
|
||||||
|
edgeMixedChunkCount.toArray(),
|
||||||
|
edgeMixedParamOff.toArray(),
|
||||||
|
edgeMixedParamCount.toArray(),
|
||||||
|
edgeMixedStrategy.toArray(),
|
||||||
|
mixedChunkOff.toArray(),
|
||||||
|
mixedChunkLen.toArray(),
|
||||||
|
mixedParamKeyId.toArray(),
|
||||||
|
acceptHandlerId.toArray(),
|
||||||
|
acceptLabelId.toArray(),
|
||||||
|
acceptRouteId.toArray(),
|
||||||
|
indexStart.toArray(),
|
||||||
|
indexCount.toArray(),
|
||||||
|
indexSecondOff.toArray(),
|
||||||
|
literalHashKey.toArray(),
|
||||||
|
literalHashEdge.toArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package dev.relism.fpr.core.internal.compile;
|
||||||
|
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@NoArgsConstructor
|
||||||
|
final class IndexBuilder {
|
||||||
|
private static final int SECOND_LEVEL_THRESHOLD = 64;
|
||||||
|
|
||||||
|
static int buildIndex(PrimitiveBuilders.IntArrayList indexStart,
|
||||||
|
PrimitiveBuilders.ShortArrayList indexCount,
|
||||||
|
PrimitiveBuilders.IntArrayList indexSecondOff,
|
||||||
|
PrimitiveBuilders.IntArrayList edgeLabelOff,
|
||||||
|
PrimitiveBuilders.ShortArrayList edgeLabelLen,
|
||||||
|
PrimitiveBuilders.ByteBlobBuilder blob,
|
||||||
|
int start,
|
||||||
|
int count,
|
||||||
|
boolean allowSecondByte) {
|
||||||
|
int base = allocateTable(indexStart, indexCount, indexSecondOff);
|
||||||
|
int[] startTmp = new int[256];
|
||||||
|
short[] countTmp = new short[256];
|
||||||
|
int[] len2Count = new int[256];
|
||||||
|
Arrays.fill(startTmp, -1);
|
||||||
|
|
||||||
|
int end = start + count;
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
int off = edgeLabelOff.get(i);
|
||||||
|
int len = edgeLabelLen.get(i);
|
||||||
|
if (len == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int first = blob.byteAt(off) & 0xFF;
|
||||||
|
if (startTmp[first] == -1) {
|
||||||
|
startTmp[first] = i - start;
|
||||||
|
}
|
||||||
|
countTmp[first]++;
|
||||||
|
if (allowSecondByte && len > 1) {
|
||||||
|
len2Count[first]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
indexStart.set(base + i, startTmp[i]);
|
||||||
|
indexCount.set(base + i, countTmp[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowSecondByte) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int first = 0; first < 256; first++) {
|
||||||
|
int bucketCount = countTmp[first];
|
||||||
|
if (bucketCount < SECOND_LEVEL_THRESHOLD || len2Count[first] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int relStart = startTmp[first];
|
||||||
|
if (relStart < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int bucketStart = start + relStart;
|
||||||
|
int bucketEnd = bucketStart + bucketCount;
|
||||||
|
|
||||||
|
int secondBase = allocateTable(indexStart, indexCount, indexSecondOff);
|
||||||
|
int[] secondStart = new int[256];
|
||||||
|
short[] secondCount = new short[256];
|
||||||
|
Arrays.fill(secondStart, -1);
|
||||||
|
|
||||||
|
for (int i = bucketStart; i < bucketEnd; i++) {
|
||||||
|
int len = edgeLabelLen.get(i);
|
||||||
|
if (len <= 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int off = edgeLabelOff.get(i);
|
||||||
|
int second = blob.byteAt(off + 1) & 0xFF;
|
||||||
|
if (secondStart[second] == -1) {
|
||||||
|
secondStart[second] = i - start;
|
||||||
|
}
|
||||||
|
secondCount[second]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
indexStart.set(secondBase + i, secondStart[i]);
|
||||||
|
indexCount.set(secondBase + i, secondCount[i]);
|
||||||
|
}
|
||||||
|
indexSecondOff.set(base + first, secondBase);
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int allocateTable(PrimitiveBuilders.IntArrayList indexStart,
|
||||||
|
PrimitiveBuilders.ShortArrayList indexCount,
|
||||||
|
PrimitiveBuilders.IntArrayList indexSecondOff) {
|
||||||
|
int base = indexStart.size();
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
indexStart.add(-1);
|
||||||
|
indexCount.add((short) 0);
|
||||||
|
indexSecondOff.add(-1);
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package dev.relism.fpr.core.internal.compile;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public final class PrimitiveBuilders {
|
||||||
|
public static final class IntArrayList {
|
||||||
|
private int[] data;
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
public IntArrayList(int initial) {
|
||||||
|
this.data = new int[Math.max(8, initial)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int get(int index) {
|
||||||
|
return data[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(int index, int value) {
|
||||||
|
data[index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(int value) {
|
||||||
|
ensure(size + 1);
|
||||||
|
data[size++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] toArray() {
|
||||||
|
int[] out = new int[size];
|
||||||
|
System.arraycopy(data, 0, out, 0, size);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensure(int target) {
|
||||||
|
if (target <= data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int newCap = Math.max(target, data.length * 2);
|
||||||
|
int[] next = new int[newCap];
|
||||||
|
System.arraycopy(data, 0, next, 0, size);
|
||||||
|
data = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ShortArrayList {
|
||||||
|
private short[] data;
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
public ShortArrayList(int initial) {
|
||||||
|
this.data = new short[Math.max(8, initial)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short get(int index) {
|
||||||
|
return data[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(int index, short value) {
|
||||||
|
data[index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(short value) {
|
||||||
|
ensure(size + 1);
|
||||||
|
data[size++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short[] toArray() {
|
||||||
|
short[] out = new short[size];
|
||||||
|
System.arraycopy(data, 0, out, 0, size);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensure(int target) {
|
||||||
|
if (target <= data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int newCap = Math.max(target, data.length * 2);
|
||||||
|
short[] next = new short[newCap];
|
||||||
|
System.arraycopy(data, 0, next, 0, size);
|
||||||
|
data = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ByteArrayList {
|
||||||
|
private byte[] data;
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
public ByteArrayList(int initial) {
|
||||||
|
this.data = new byte[Math.max(8, initial)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(byte value) {
|
||||||
|
ensure(size + 1);
|
||||||
|
data[size++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] toArray() {
|
||||||
|
byte[] out = new byte[size];
|
||||||
|
System.arraycopy(data, 0, out, 0, size);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensure(int target) {
|
||||||
|
if (target <= data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int newCap = Math.max(target, data.length * 2);
|
||||||
|
byte[] next = new byte[newCap];
|
||||||
|
System.arraycopy(data, 0, next, 0, size);
|
||||||
|
data = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class LongArrayList {
|
||||||
|
private long[] data;
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
public LongArrayList(int initial) {
|
||||||
|
this.data = new long[Math.max(8, initial)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long get(int index) {
|
||||||
|
return data[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(int index, long value) {
|
||||||
|
data[index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(long value) {
|
||||||
|
ensure(size + 1);
|
||||||
|
data[size++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long[] toArray() {
|
||||||
|
long[] out = new long[size];
|
||||||
|
System.arraycopy(data, 0, out, 0, size);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensure(int target) {
|
||||||
|
if (target <= data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int newCap = Math.max(target, data.length * 2);
|
||||||
|
long[] next = new long[newCap];
|
||||||
|
System.arraycopy(data, 0, next, 0, size);
|
||||||
|
data = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ByteBlobBuilder {
|
||||||
|
private byte[] data;
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
public ByteBlobBuilder(int initial) {
|
||||||
|
this.data = new byte[Math.max(16, initial)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int append(byte[] bytes) {
|
||||||
|
int off = size;
|
||||||
|
ensure(size + bytes.length);
|
||||||
|
System.arraycopy(bytes, 0, data, size, bytes.length);
|
||||||
|
size += bytes.length;
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte byteAt(int index) {
|
||||||
|
return data[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] toArray() {
|
||||||
|
byte[] out = new byte[size];
|
||||||
|
System.arraycopy(data, 0, out, 0, size);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensure(int target) {
|
||||||
|
if (target <= data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int newCap = Math.max(target, data.length * 2);
|
||||||
|
byte[] next = new byte[newCap];
|
||||||
|
System.arraycopy(data, 0, next, 0, size);
|
||||||
|
data = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package dev.relism.fpr.core.internal.compile;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.fpr.core.FastPathRouter;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.FrozenRouter;
|
||||||
|
import dev.relism.fpr.core.RouterBuilder;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class RouteCompiler {
|
||||||
|
private RouteCompiler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <H> FastPathRouter<ByteView, H> compile(List<RouterBuilder.RouteSpec<H>> routes,
|
||||||
|
Map<String, Integer> paramIds) {
|
||||||
|
if (routes.isEmpty()) {
|
||||||
|
return emptyRouter();
|
||||||
|
}
|
||||||
|
RouteGraph<H> graph = RouteGraph.build(routes, paramIds).canonicalize();
|
||||||
|
return FreezeWriter.freeze(graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <H> FastPathRouter<ByteView, H> emptyRouter() {
|
||||||
|
return new FrozenRouter<>(
|
||||||
|
new byte[0],
|
||||||
|
(H[]) new Object[0],
|
||||||
|
new int[]{0}, // stateFirstEdge
|
||||||
|
new short[]{0}, // stateEdgeCount
|
||||||
|
new int[]{0}, // stateLiteralStart
|
||||||
|
new short[]{0}, // stateLiteralCount
|
||||||
|
new byte[]{0}, // stateLiteralStrategy
|
||||||
|
new int[]{-1}, // stateLiteralHashOff
|
||||||
|
new int[]{-1}, // stateLiteralHashMask
|
||||||
|
new int[]{0}, // stateMixedStart
|
||||||
|
new short[]{0}, // stateMixedCount
|
||||||
|
new short[]{0}, // stateMixedPrefixCount
|
||||||
|
new int[]{-1}, // stateWildIndex
|
||||||
|
new int[]{-1}, // stateParamNext
|
||||||
|
new short[]{-1}, // stateParamKeyId
|
||||||
|
new int[]{-1}, // stateCatchAllNext
|
||||||
|
new short[]{-1}, // stateCatchAllKeyId
|
||||||
|
new int[]{0}, // stateAcceptFirst
|
||||||
|
new short[]{0}, // stateAcceptCount
|
||||||
|
new int[]{-1}, // stateLiteralIndexOff
|
||||||
|
new int[]{-1}, // stateMixedIndexOff
|
||||||
|
new int[0], // edgeNextState
|
||||||
|
new int[0], // edgeLabelOff
|
||||||
|
new short[0], // edgeLabelLen
|
||||||
|
new long[0], // edgeLiteralPrefix
|
||||||
|
new byte[0], // edgeKind
|
||||||
|
new int[0], // edgeMixedChunkOff
|
||||||
|
new short[0], // edgeMixedChunkCount
|
||||||
|
new int[0], // edgeMixedParamOff
|
||||||
|
new short[0], // edgeMixedParamCount
|
||||||
|
new byte[0], // edgeMixedStrategy
|
||||||
|
new int[0], // mixedChunkOff
|
||||||
|
new short[0], // mixedChunkLen
|
||||||
|
new short[0], // mixedParamKeyId
|
||||||
|
new int[0], // acceptHandlerId
|
||||||
|
new int[0], // acceptLabelId
|
||||||
|
new int[0], // acceptRouteId
|
||||||
|
new int[0], // indexStart
|
||||||
|
new short[0], // indexCount
|
||||||
|
new int[0], // indexSecondOff
|
||||||
|
new long[0], // literalHashKey
|
||||||
|
new int[0] // literalHashEdge
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,558 @@
|
|||||||
|
package dev.relism.fpr.core.internal.compile;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.RoutePattern;
|
||||||
|
import dev.relism.fpr.core.RouterBuilder;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.EdgeKind;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
final class RouteGraph<H> {
|
||||||
|
final List<Node<H>> nodes;
|
||||||
|
final List<H> handlers;
|
||||||
|
|
||||||
|
private RouteGraph(List<Node<H>> nodes, List<H> handlers) {
|
||||||
|
this.nodes = nodes;
|
||||||
|
this.handlers = handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
static <H> RouteGraph<H> build(List<RouterBuilder.RouteSpec<H>> routes, Map<String, Integer> paramIds) {
|
||||||
|
List<RouterBuilder.RouteSpec<H>> ordered = new ArrayList<>(routes);
|
||||||
|
ordered.sort(routeComparator());
|
||||||
|
|
||||||
|
List<Node<H>> nodes = new ArrayList<>();
|
||||||
|
Node<H> root = new Node<>(0);
|
||||||
|
nodes.add(root);
|
||||||
|
|
||||||
|
List<H> handlers = new ArrayList<>();
|
||||||
|
int routeId = 0;
|
||||||
|
|
||||||
|
for (RouterBuilder.RouteSpec<H> spec : ordered) {
|
||||||
|
int handlerId = handlers.size();
|
||||||
|
handlers.add(spec.handler());
|
||||||
|
Node<H> current = root;
|
||||||
|
List<RoutePattern.Segment> segments = spec.pattern().segments();
|
||||||
|
for (int i = 0; i < segments.size(); i++) {
|
||||||
|
RoutePattern.Segment segment = segments.get(i);
|
||||||
|
boolean last = i == segments.size() - 1;
|
||||||
|
switch (segment.type()) {
|
||||||
|
case LITERAL:
|
||||||
|
current = current.literal(nodes, literalBytes((RoutePattern.Literal) segment));
|
||||||
|
break;
|
||||||
|
case MIXED:
|
||||||
|
current = current.mixed(nodes, mixedDef((RoutePattern.Mixed) segment, paramIds));
|
||||||
|
break;
|
||||||
|
case PARAM:
|
||||||
|
int paramId = paramId(((RoutePattern.Param) segment).name(), paramIds);
|
||||||
|
current = current.param(nodes, paramId);
|
||||||
|
break;
|
||||||
|
case WILDCARD:
|
||||||
|
current = current.wild(nodes);
|
||||||
|
break;
|
||||||
|
case CATCH_ALL:
|
||||||
|
if (!last) {
|
||||||
|
throw new IllegalArgumentException("catch-all must be last segment");
|
||||||
|
}
|
||||||
|
Integer catchId = catchId((RoutePattern.CatchAll) segment, paramIds);
|
||||||
|
current = current.catchAll(nodes, catchId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unhandled segment type: " + segment.type());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current.accept(spec.labelId(), handlerId, routeId);
|
||||||
|
routeId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteGraph<>(nodes, handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
RouteGraph<H> canonicalize() {
|
||||||
|
int size = nodes.size();
|
||||||
|
int[] remap = new int[size];
|
||||||
|
Arrays.fill(remap, -1);
|
||||||
|
Map<NodeKey, Integer> canonical = new HashMap<>();
|
||||||
|
List<Node<H>> canonNodes = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = size - 1; i >= 0; i--) {
|
||||||
|
Node<H> node = nodes.get(i);
|
||||||
|
NodeKey key = node.key(remap);
|
||||||
|
Integer existing = canonical.get(key);
|
||||||
|
if (existing != null) {
|
||||||
|
remap[node.id] = existing;
|
||||||
|
} else {
|
||||||
|
int id = canonNodes.size();
|
||||||
|
remap[node.id] = id;
|
||||||
|
canonical.put(key, id);
|
||||||
|
canonNodes.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Node<H> node : canonNodes) {
|
||||||
|
node.remap(remap);
|
||||||
|
}
|
||||||
|
int rootIndex = remap[0];
|
||||||
|
if (rootIndex != 0) {
|
||||||
|
int[] reorder = new int[canonNodes.size()];
|
||||||
|
Arrays.fill(reorder, -1);
|
||||||
|
reorder[rootIndex] = 0;
|
||||||
|
int next = 1;
|
||||||
|
for (int i = 0; i < canonNodes.size(); i++) {
|
||||||
|
if (i == rootIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reorder[i] = next++;
|
||||||
|
}
|
||||||
|
List<Node<H>> reordered = new ArrayList<>(canonNodes.size());
|
||||||
|
for (int i = 0; i < canonNodes.size(); i++) {
|
||||||
|
reordered.add(null);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < canonNodes.size(); i++) {
|
||||||
|
reordered.set(reorder[i], canonNodes.get(i));
|
||||||
|
}
|
||||||
|
for (Node<H> node : reordered) {
|
||||||
|
node.remap(reorder);
|
||||||
|
}
|
||||||
|
return new RouteGraph<>(reordered, handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteGraph<>(canonNodes, handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int paramId(String name, Map<String, Integer> paramIds) {
|
||||||
|
Integer id = paramIds.get(name);
|
||||||
|
if (id == null) {
|
||||||
|
throw new IllegalArgumentException("Unknown param id: " + name);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer catchId(RoutePattern.CatchAll segment, Map<String, Integer> paramIds) {
|
||||||
|
String name = segment.name();
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return paramId(name, paramIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] literalBytes(RoutePattern.Literal literal) {
|
||||||
|
return literal.text().getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MixedDef mixedDef(RoutePattern.Mixed mixed, Map<String, Integer> paramIds) {
|
||||||
|
String[] literals = mixed.literals();
|
||||||
|
String[] params = mixed.params();
|
||||||
|
byte[][] literalBytes = new byte[literals.length][];
|
||||||
|
for (int i = 0; i < literals.length; i++) {
|
||||||
|
literalBytes[i] = literals[i].getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
short[] keys = new short[params.length];
|
||||||
|
for (int i = 0; i < params.length; i++) {
|
||||||
|
keys[i] = (short) paramId(params[i], paramIds);
|
||||||
|
}
|
||||||
|
return new MixedDef(literalBytes, keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Comparator<RouterBuilder.RouteSpec<?>> routeComparator() {
|
||||||
|
return (a, b) -> {
|
||||||
|
List<RoutePattern.Segment> segA = a.pattern().segments();
|
||||||
|
List<RoutePattern.Segment> segB = b.pattern().segments();
|
||||||
|
int min = Math.min(segA.size(), segB.size());
|
||||||
|
for (int i = 0; i < min; i++) {
|
||||||
|
int rankA = rank(segA.get(i).type());
|
||||||
|
int rankB = rank(segB.get(i).type());
|
||||||
|
if (rankA != rankB) {
|
||||||
|
return Integer.compare(rankB, rankA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (segA.size() != segB.size()) {
|
||||||
|
return Integer.compare(segB.size(), segA.size());
|
||||||
|
}
|
||||||
|
return Integer.compare(a.order(), b.order());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int rank(RoutePattern.SegmentType type) {
|
||||||
|
switch (type) {
|
||||||
|
case LITERAL:
|
||||||
|
return 5;
|
||||||
|
case MIXED:
|
||||||
|
return 4;
|
||||||
|
case PARAM:
|
||||||
|
return 3;
|
||||||
|
case WILDCARD:
|
||||||
|
return 2;
|
||||||
|
case CATCH_ALL:
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class MixedDef {
|
||||||
|
final byte[][] literals;
|
||||||
|
final short[] paramKeys;
|
||||||
|
|
||||||
|
MixedDef(byte[][] literals, short[] paramKeys) {
|
||||||
|
this.literals = literals;
|
||||||
|
this.paramKeys = paramKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(obj instanceof MixedDef)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MixedDef other = (MixedDef) obj;
|
||||||
|
return Arrays.deepEquals(literals, other.literals) && Arrays.equals(paramKeys, other.paramKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int h = Arrays.deepHashCode(literals);
|
||||||
|
h = 31 * h + Arrays.hashCode(paramKeys);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class Accept {
|
||||||
|
final int labelId;
|
||||||
|
final int handlerId;
|
||||||
|
final int routeId;
|
||||||
|
|
||||||
|
Accept(int labelId, int handlerId, int routeId) {
|
||||||
|
this.labelId = labelId;
|
||||||
|
this.handlerId = handlerId;
|
||||||
|
this.routeId = routeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class Edge {
|
||||||
|
final EdgeKind kind;
|
||||||
|
final byte[] literal;
|
||||||
|
final MixedDef mixed;
|
||||||
|
int nextState;
|
||||||
|
|
||||||
|
Edge(EdgeKind kind, byte[] literal, MixedDef mixed, int nextState) {
|
||||||
|
this.kind = kind;
|
||||||
|
this.literal = literal;
|
||||||
|
this.mixed = mixed;
|
||||||
|
this.nextState = nextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Comparator<Edge> literalComparator() {
|
||||||
|
return (a, b) -> {
|
||||||
|
if (a.literal.length != b.literal.length) {
|
||||||
|
return Integer.compare(a.literal.length, b.literal.length);
|
||||||
|
}
|
||||||
|
long aKey = literalPrefix(a.literal);
|
||||||
|
long bKey = literalPrefix(b.literal);
|
||||||
|
int cmp = Long.compareUnsigned(aKey, bKey);
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
return Arrays.compare(a.literal, b.literal);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Comparator<Edge> mixedComparator() {
|
||||||
|
return (a, b) -> {
|
||||||
|
int aTotal = totalLiteralLen(a.mixed.literals);
|
||||||
|
int bTotal = totalLiteralLen(b.mixed.literals);
|
||||||
|
if (aTotal != bTotal) {
|
||||||
|
return Integer.compare(bTotal, aTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] aFirst = a.mixed.literals[0];
|
||||||
|
byte[] bFirst = b.mixed.literals[0];
|
||||||
|
int aLen = aFirst.length;
|
||||||
|
int bLen = bFirst.length;
|
||||||
|
if (aLen == 0 && bLen != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (aLen != 0 && bLen == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (aLen != bLen) {
|
||||||
|
return Integer.compare(bLen, aLen);
|
||||||
|
}
|
||||||
|
int cmp = Arrays.compare(aFirst, bFirst);
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
int count = Math.min(a.mixed.literals.length, b.mixed.literals.length);
|
||||||
|
for (int i = 1; i < count; i++) {
|
||||||
|
cmp = Arrays.compare(a.mixed.literals[i], b.mixed.literals[i]);
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Integer.compare(b.mixed.literals.length, a.mixed.literals.length);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int totalLiteralLen(byte[][] literals) {
|
||||||
|
int total = 0;
|
||||||
|
for (byte[] literal : literals) {
|
||||||
|
total += literal.length;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long literalPrefix(byte[] literal) {
|
||||||
|
int len = Math.min(8, literal.length);
|
||||||
|
long key = 0;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
key |= ((long) literal[i] & 0xFFL) << (i * 8);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class Node<H> {
|
||||||
|
final int id;
|
||||||
|
final List<Edge> edges = new ArrayList<>();
|
||||||
|
int paramNext = -1;
|
||||||
|
short paramKeyId = -1;
|
||||||
|
int catchAllNext = -1;
|
||||||
|
short catchAllKeyId = -1;
|
||||||
|
final List<Accept> accepts = new ArrayList<>();
|
||||||
|
|
||||||
|
Node(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node<H> literal(List<Node<H>> nodes, byte[] literal) {
|
||||||
|
for (Edge edge : edges) {
|
||||||
|
if (edge.kind == EdgeKind.LITERAL && Arrays.equals(edge.literal, literal)) {
|
||||||
|
return nodes.get(edge.nextState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node<H> next = new Node<>(nodes.size());
|
||||||
|
nodes.add(next);
|
||||||
|
edges.add(new Edge(EdgeKind.LITERAL, literal, null, next.id));
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node<H> mixed(List<Node<H>> nodes, MixedDef def) {
|
||||||
|
for (Edge edge : edges) {
|
||||||
|
if (edge.kind == EdgeKind.MIXED && mixedEquals(edge.mixed, def)) {
|
||||||
|
if (!Arrays.equals(edge.mixed.paramKeys, def.paramKeys)) {
|
||||||
|
throw new IllegalArgumentException("Ambiguous mixed segment: conflicting param keys");
|
||||||
|
}
|
||||||
|
return nodes.get(edge.nextState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node<H> next = new Node<>(nodes.size());
|
||||||
|
nodes.add(next);
|
||||||
|
edges.add(new Edge(EdgeKind.MIXED, null, def, next.id));
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node<H> param(List<Node<H>> nodes, int keyId) {
|
||||||
|
if (paramNext != -1) {
|
||||||
|
if (paramKeyId != (short) keyId) {
|
||||||
|
throw new IllegalArgumentException("Ambiguous param segment at state " + id);
|
||||||
|
}
|
||||||
|
return nodes.get(paramNext);
|
||||||
|
}
|
||||||
|
Node<H> next = new Node<>(nodes.size());
|
||||||
|
nodes.add(next);
|
||||||
|
paramNext = next.id;
|
||||||
|
paramKeyId = (short) keyId;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node<H> wild(List<Node<H>> nodes) {
|
||||||
|
for (Edge edge : edges) {
|
||||||
|
if (edge.kind == EdgeKind.WILD) {
|
||||||
|
return nodes.get(edge.nextState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node<H> next = new Node<>(nodes.size());
|
||||||
|
nodes.add(next);
|
||||||
|
edges.add(new Edge(EdgeKind.WILD, null, null, next.id));
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node<H> catchAll(List<Node<H>> nodes, Integer keyId) {
|
||||||
|
if (catchAllNext != -1) {
|
||||||
|
short existing = catchAllKeyId;
|
||||||
|
short incoming = keyId == null ? -1 : keyId.shortValue();
|
||||||
|
if (existing != incoming) {
|
||||||
|
throw new IllegalArgumentException("Ambiguous catch-all segment at state " + id);
|
||||||
|
}
|
||||||
|
return nodes.get(catchAllNext);
|
||||||
|
}
|
||||||
|
Node<H> next = new Node<>(nodes.size());
|
||||||
|
nodes.add(next);
|
||||||
|
catchAllNext = next.id;
|
||||||
|
catchAllKeyId = keyId == null ? (short) -1 : keyId.shortValue();
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
void accept(int labelId, int handlerId, int routeId) {
|
||||||
|
for (Accept accept : accepts) {
|
||||||
|
if (accept.labelId == labelId) {
|
||||||
|
throw new IllegalArgumentException("Ambiguous route: duplicate pattern for label " + labelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accepts.add(new Accept(labelId, handlerId, routeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Edge> literalEdges() {
|
||||||
|
List<Edge> out = new ArrayList<>();
|
||||||
|
for (Edge edge : edges) {
|
||||||
|
if (edge.kind == EdgeKind.LITERAL) {
|
||||||
|
out.add(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Edge> mixedEdges() {
|
||||||
|
List<Edge> out = new ArrayList<>();
|
||||||
|
for (Edge edge : edges) {
|
||||||
|
if (edge.kind == EdgeKind.MIXED) {
|
||||||
|
out.add(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Edge wildEdge() {
|
||||||
|
for (Edge edge : edges) {
|
||||||
|
if (edge.kind == EdgeKind.WILD) {
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeKey key(int[] remap) {
|
||||||
|
return new NodeKey(this, remap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remap(int[] remap) {
|
||||||
|
for (Edge edge : edges) {
|
||||||
|
edge.nextState = remap[edge.nextState];
|
||||||
|
}
|
||||||
|
if (paramNext != -1) {
|
||||||
|
paramNext = remap[paramNext];
|
||||||
|
}
|
||||||
|
if (catchAllNext != -1) {
|
||||||
|
catchAllNext = remap[catchAllNext];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class NodeKey {
|
||||||
|
private final int hash;
|
||||||
|
private final EdgeKind[] edgeKinds;
|
||||||
|
private final int[] edgeNext;
|
||||||
|
private final byte[][] edgeLiterals;
|
||||||
|
private final MixedDef[] edgeMixed;
|
||||||
|
private final int paramNext;
|
||||||
|
private final short paramKeyId;
|
||||||
|
private final int catchNext;
|
||||||
|
private final short catchKeyId;
|
||||||
|
private final int[] acceptMethods;
|
||||||
|
private final int[] acceptHandlers;
|
||||||
|
private final int[] acceptRoutes;
|
||||||
|
|
||||||
|
private NodeKey(Node<?> node, int[] remap) {
|
||||||
|
this.paramNext = node.paramNext == -1 ? -1 : remap[node.paramNext];
|
||||||
|
this.paramKeyId = node.paramKeyId;
|
||||||
|
this.catchNext = node.catchAllNext == -1 ? -1 : remap[node.catchAllNext];
|
||||||
|
this.catchKeyId = node.catchAllKeyId;
|
||||||
|
|
||||||
|
int edgeCount = node.edges.size();
|
||||||
|
this.edgeKinds = new EdgeKind[edgeCount];
|
||||||
|
this.edgeNext = new int[edgeCount];
|
||||||
|
this.edgeLiterals = new byte[edgeCount][];
|
||||||
|
this.edgeMixed = new MixedDef[edgeCount];
|
||||||
|
for (int i = 0; i < edgeCount; i++) {
|
||||||
|
Edge edge = node.edges.get(i);
|
||||||
|
edgeKinds[i] = edge.kind;
|
||||||
|
edgeNext[i] = remap[edge.nextState];
|
||||||
|
edgeLiterals[i] = edge.literal;
|
||||||
|
edgeMixed[i] = edge.mixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int acceptCount = node.accepts.size();
|
||||||
|
acceptMethods = new int[acceptCount];
|
||||||
|
acceptHandlers = new int[acceptCount];
|
||||||
|
acceptRoutes = new int[acceptCount];
|
||||||
|
for (int i = 0; i < acceptCount; i++) {
|
||||||
|
Accept accept = node.accepts.get(i);
|
||||||
|
acceptMethods[i] = accept.labelId;
|
||||||
|
acceptHandlers[i] = accept.handlerId;
|
||||||
|
acceptRoutes[i] = accept.routeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hash = computeHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int computeHash() {
|
||||||
|
int h = 1;
|
||||||
|
h = 31 * h + paramNext;
|
||||||
|
h = 31 * h + paramKeyId;
|
||||||
|
h = 31 * h + catchNext;
|
||||||
|
h = 31 * h + catchKeyId;
|
||||||
|
h = 31 * h + Arrays.hashCode(edgeKinds);
|
||||||
|
h = 31 * h + Arrays.hashCode(edgeNext);
|
||||||
|
h = 31 * h + Arrays.deepHashCode(edgeLiterals);
|
||||||
|
h = 31 * h + Arrays.deepHashCode(edgeMixed);
|
||||||
|
h = 31 * h + Arrays.hashCode(acceptMethods);
|
||||||
|
h = 31 * h + Arrays.hashCode(acceptHandlers);
|
||||||
|
h = 31 * h + Arrays.hashCode(acceptRoutes);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(obj instanceof NodeKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
NodeKey other = (NodeKey) obj;
|
||||||
|
if (paramNext != other.paramNext || paramKeyId != other.paramKeyId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (catchNext != other.catchNext || catchKeyId != other.catchKeyId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(edgeKinds, other.edgeKinds) || !Arrays.equals(edgeNext, other.edgeNext)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Arrays.deepEquals(edgeLiterals, other.edgeLiterals)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Arrays.deepEquals(edgeMixed, other.edgeMixed)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Arrays.equals(acceptMethods, other.acceptMethods)
|
||||||
|
&& Arrays.equals(acceptHandlers, other.acceptHandlers)
|
||||||
|
&& Arrays.equals(acceptRoutes, other.acceptRoutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean mixedEquals(MixedDef a, MixedDef b) {
|
||||||
|
return Arrays.deepEquals(a.literals, b.literals);
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
package dev.relism.fpr.core.internal.compile.lookup;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.internal.compile.PrimitiveBuilders;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.lookup.LiteralLookupStrategy;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public final class LiteralLookupPlanBuilder {
|
||||||
|
private static final int LINEAR_LIMIT = 16;
|
||||||
|
private static final int ORDERED_LIMIT = 256;
|
||||||
|
|
||||||
|
public static byte selectStrategy(int literalCount) {
|
||||||
|
if (literalCount <= LINEAR_LIMIT) {
|
||||||
|
return LiteralLookupStrategy.LINEAR;
|
||||||
|
}
|
||||||
|
if (literalCount <= ORDERED_LIMIT) {
|
||||||
|
return LiteralLookupStrategy.ORDERED_PREFIX;
|
||||||
|
}
|
||||||
|
return LiteralLookupStrategy.HASH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long prefixKey(byte[] literal) {
|
||||||
|
int len = Math.min(8, literal.length);
|
||||||
|
long key = 0;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
key |= ((long) literal[i] & 0xFFL) << (i * 8);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HashPlan buildHash(PrimitiveBuilders.LongArrayList hashKeys,
|
||||||
|
PrimitiveBuilders.IntArrayList hashEdges,
|
||||||
|
PrimitiveBuilders.LongArrayList edgePrefix,
|
||||||
|
PrimitiveBuilders.ShortArrayList edgeLabelLen,
|
||||||
|
int start,
|
||||||
|
int count) {
|
||||||
|
int size = tableSize(count);
|
||||||
|
int mask = size - 1;
|
||||||
|
int off = hashKeys.size();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
hashKeys.add(0L);
|
||||||
|
hashEdges.add(-1);
|
||||||
|
}
|
||||||
|
int end = start + count;
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
int len = edgeLabelLen.get(i);
|
||||||
|
if (len <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long key = edgePrefix.get(i);
|
||||||
|
int slot = mix(key) & mask;
|
||||||
|
while (hashKeys.get(off + slot) != 0L) {
|
||||||
|
slot = (slot + 1) & mask;
|
||||||
|
}
|
||||||
|
hashKeys.set(off + slot, key);
|
||||||
|
hashEdges.set(off + slot, i);
|
||||||
|
}
|
||||||
|
return new HashPlan(off, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int tableSize(int count) {
|
||||||
|
int size = 1;
|
||||||
|
int target = Math.max(4, count * 2);
|
||||||
|
while (size < target) {
|
||||||
|
size <<= 1;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int mix(long key) {
|
||||||
|
long h = key ^ (key >>> 33);
|
||||||
|
h *= 0xff51afd7ed558ccdL;
|
||||||
|
h ^= (h >>> 33);
|
||||||
|
h *= 0xc4ceb9fe1a85ec53L;
|
||||||
|
h ^= (h >>> 33);
|
||||||
|
return (int) h;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public static final class HashPlan {
|
||||||
|
public final int offset;
|
||||||
|
public final int mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package dev.relism.fpr.core.internal.compile.lookup;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.internal.runtime.lookup.MixedLookupStrategy;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public final class MixedLookupPlanBuilder {
|
||||||
|
public static byte strategyForParamCount(int paramCount) {
|
||||||
|
if (paramCount <= 1) {
|
||||||
|
return MixedLookupStrategy.ONE;
|
||||||
|
}
|
||||||
|
if (paramCount == 2) {
|
||||||
|
return MixedLookupStrategy.TWO;
|
||||||
|
}
|
||||||
|
return MixedLookupStrategy.N;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.VarHandle;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public final class ByteCompare {
|
||||||
|
private static final VarHandle LONG_VIEW = MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
public static boolean equals(ByteView view, int start, byte[] blob, int off, int len, boolean supportsLong) {
|
||||||
|
if (len == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int i = 0;
|
||||||
|
if (supportsLong && len >= 8) {
|
||||||
|
int end = len - 8;
|
||||||
|
for (; i <= end; i += 8) {
|
||||||
|
long a = view.longAt(start + i);
|
||||||
|
long b = (long) LONG_VIEW.get(blob, off + i);
|
||||||
|
if (a != b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (; i < len; i++) {
|
||||||
|
if (view.byteAt(start + i) != blob[off + i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int indexOf(ByteView view, int start, int max, byte[] blob, int off, int len, boolean supportsLong) {
|
||||||
|
if (len == 0) {
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
byte first = blob[off];
|
||||||
|
for (int i = start; i <= max; i++) {
|
||||||
|
if (view.byteAt(i) != first) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (equals(view, i, blob, off, len, supportsLong)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime;
|
||||||
|
|
||||||
|
final class EdgeDispatch {
|
||||||
|
private EdgeDispatch() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static long mixedPrefixRange(FrozenRouter<?> router, int state, int firstByte) {
|
||||||
|
int prefixCount = router.stateMixedPrefixCount[state];
|
||||||
|
if (prefixCount <= 0) {
|
||||||
|
return range(0, 0);
|
||||||
|
}
|
||||||
|
int indexOff = router.stateMixedIndexOff[state];
|
||||||
|
if (indexOff >= 0) {
|
||||||
|
int rangeStartRel = router.indexStart[indexOff + firstByte];
|
||||||
|
int rangeCount = router.indexCount[indexOff + firstByte];
|
||||||
|
if (rangeCount == 0 || rangeStartRel < 0) {
|
||||||
|
return range(0, 0);
|
||||||
|
}
|
||||||
|
return range(router.stateMixedStart[state] + rangeStartRel, rangeCount);
|
||||||
|
}
|
||||||
|
return range(router.stateMixedStart[state], router.stateMixedCount[state]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static long range(int start, int count) {
|
||||||
|
return ((long) start << 32) | (count & 0xffffffffL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int rangeStart(long range) {
|
||||||
|
return (int) (range >>> 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int rangeCount(long range) {
|
||||||
|
return (int) range;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime;
|
||||||
|
|
||||||
|
public enum EdgeKind {
|
||||||
|
LITERAL,
|
||||||
|
MIXED,
|
||||||
|
PARAM,
|
||||||
|
WILD,
|
||||||
|
CATCH
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.fpr.core.FastPathRouter;
|
||||||
|
import dev.relism.fpr.core.MatchResult;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.lookup.MixedLookup;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public final class FrozenRouter<H> implements FastPathRouter<ByteView, H> {
|
||||||
|
public final byte[] blob;
|
||||||
|
public final H[] handlers;
|
||||||
|
|
||||||
|
public final int[] stateFirstEdge;
|
||||||
|
public final short[] stateEdgeCount;
|
||||||
|
public final int[] stateLiteralStart;
|
||||||
|
public final short[] stateLiteralCount;
|
||||||
|
public final byte[] stateLiteralStrategy;
|
||||||
|
public final int[] stateLiteralHashOff;
|
||||||
|
public final int[] stateLiteralHashMask;
|
||||||
|
public final int[] stateMixedStart;
|
||||||
|
public final short[] stateMixedCount;
|
||||||
|
public final short[] stateMixedPrefixCount;
|
||||||
|
public final int[] stateWildIndex;
|
||||||
|
|
||||||
|
public final int[] stateParamNext;
|
||||||
|
public final short[] stateParamKeyId;
|
||||||
|
public final int[] stateCatchAllNext;
|
||||||
|
public final short[] stateCatchAllKeyId;
|
||||||
|
|
||||||
|
public final int[] stateAcceptFirst;
|
||||||
|
public final short[] stateAcceptCount;
|
||||||
|
|
||||||
|
public final int[] stateLiteralIndexOff;
|
||||||
|
public final int[] stateMixedIndexOff;
|
||||||
|
|
||||||
|
public final int[] edgeNextState;
|
||||||
|
public final int[] edgeLabelOff;
|
||||||
|
public final short[] edgeLabelLen;
|
||||||
|
public final long[] edgeLiteralPrefix;
|
||||||
|
public final byte[] edgeKind;
|
||||||
|
public final int[] edgeMixedChunkOff;
|
||||||
|
public final short[] edgeMixedChunkCount;
|
||||||
|
public final int[] edgeMixedParamOff;
|
||||||
|
public final short[] edgeMixedParamCount;
|
||||||
|
public final byte[] edgeMixedStrategy;
|
||||||
|
|
||||||
|
public final int[] mixedChunkOff;
|
||||||
|
public final short[] mixedChunkLen;
|
||||||
|
public final short[] mixedParamKeyId;
|
||||||
|
|
||||||
|
public final int[] acceptHandlerId;
|
||||||
|
public final int[] acceptLabelId;
|
||||||
|
public final int[] acceptRouteId;
|
||||||
|
|
||||||
|
public final int[] indexStart;
|
||||||
|
public final short[] indexCount;
|
||||||
|
public final int[] indexSecondOff;
|
||||||
|
public final long[] literalHashKey;
|
||||||
|
public final int[] literalHashEdge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches without allocations, using the provided reusable MatchResult buffer.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int match(ByteView input, MatchResult<H> out) {
|
||||||
|
RouteSearch search = new RouteSearch();
|
||||||
|
SegmentCursor cursor = new SegmentCursor();
|
||||||
|
out.reset();
|
||||||
|
boolean supportsLong = input.supportsLong();
|
||||||
|
cursor.reset(input);
|
||||||
|
search.reset(out);
|
||||||
|
RouteSearch.Frame frame = search.frame();
|
||||||
|
|
||||||
|
int len = cursor.length();
|
||||||
|
int state = 0;
|
||||||
|
boolean hasSegment = cursor.advance();
|
||||||
|
|
||||||
|
final byte kindLiteral = (byte) EdgeKind.LITERAL.ordinal();
|
||||||
|
final byte kindMixed = (byte) EdgeKind.MIXED.ordinal();
|
||||||
|
final byte kindParam = (byte) EdgeKind.PARAM.ordinal();
|
||||||
|
final byte kindWild = (byte) EdgeKind.WILD.ordinal();
|
||||||
|
final byte kindCatch = (byte) EdgeKind.CATCH.ordinal();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (!hasSegment) {
|
||||||
|
int accept = accept(state, out);
|
||||||
|
if (accept != NO_MATCH) {
|
||||||
|
return accept;
|
||||||
|
}
|
||||||
|
if (stateCatchAllNext[state] != -1) {
|
||||||
|
short key = stateCatchAllKeyId[state];
|
||||||
|
if (key >= 0) {
|
||||||
|
out.addParam(key, len, 0);
|
||||||
|
}
|
||||||
|
return accept(stateCatchAllNext[state], out);
|
||||||
|
}
|
||||||
|
long backtracked = backtrack(frame, input, supportsLong, out,
|
||||||
|
kindLiteral, kindMixed, kindParam, kindWild, kindCatch, len, search, cursor);
|
||||||
|
if (backtracked == BACKTRACK_NO_MATCH) {
|
||||||
|
return NO_MATCH;
|
||||||
|
}
|
||||||
|
if ((backtracked & BACKTRACK_ACCEPT_MASK) != 0) {
|
||||||
|
return (int) backtracked;
|
||||||
|
}
|
||||||
|
state = (int) backtracked;
|
||||||
|
hasSegment = cursor.advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int segStart = cursor.segStart();
|
||||||
|
int segLen = cursor.segLen();
|
||||||
|
|
||||||
|
if (segLen <= 0) {
|
||||||
|
long backtracked = backtrack(frame, input, supportsLong, out,
|
||||||
|
kindLiteral, kindMixed, kindParam, kindWild, kindCatch, len, search, cursor);
|
||||||
|
if (backtracked == BACKTRACK_NO_MATCH) {
|
||||||
|
return NO_MATCH;
|
||||||
|
}
|
||||||
|
if ((backtracked & BACKTRACK_ACCEPT_MASK) != 0) {
|
||||||
|
return (int) backtracked;
|
||||||
|
}
|
||||||
|
state = (int) backtracked;
|
||||||
|
hasSegment = cursor.advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long candidate = search.selectCandidate(this, state, input, cursor, supportsLong, out);
|
||||||
|
if (candidate == RouteSearch.NO_CANDIDATE) {
|
||||||
|
long backtracked = backtrack(frame, input, supportsLong, out,
|
||||||
|
kindLiteral, kindMixed, kindParam, kindWild, kindCatch, len, search, cursor);
|
||||||
|
if (backtracked == BACKTRACK_NO_MATCH) {
|
||||||
|
return NO_MATCH;
|
||||||
|
}
|
||||||
|
if ((backtracked & BACKTRACK_ACCEPT_MASK) != 0) {
|
||||||
|
return (int) backtracked;
|
||||||
|
}
|
||||||
|
state = (int) backtracked;
|
||||||
|
hasSegment = cursor.advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte kind = RouteSearch.kind(candidate);
|
||||||
|
int edgeIndex = RouteSearch.edgeIndex(candidate);
|
||||||
|
|
||||||
|
if (kind == kindLiteral) {
|
||||||
|
state = edgeNextState[edgeIndex];
|
||||||
|
} else if (kind == kindMixed) {
|
||||||
|
state = edgeNextState[edgeIndex];
|
||||||
|
} else if (kind == kindParam) {
|
||||||
|
out.addParam(stateParamKeyId[state], segStart, segLen);
|
||||||
|
state = stateParamNext[state];
|
||||||
|
} else if (kind == kindWild) {
|
||||||
|
state = edgeNextState[edgeIndex];
|
||||||
|
} else if (kind == kindCatch) {
|
||||||
|
short key = stateCatchAllKeyId[state];
|
||||||
|
if (key >= 0) {
|
||||||
|
out.addParam(key, segStart, len - segStart);
|
||||||
|
}
|
||||||
|
return accept(stateCatchAllNext[state], out);
|
||||||
|
} else {
|
||||||
|
state = NO_MATCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == NO_MATCH) {
|
||||||
|
long backtracked = backtrack(frame, input, supportsLong, out,
|
||||||
|
kindLiteral, kindMixed, kindParam, kindWild, kindCatch, len, search, cursor);
|
||||||
|
if (backtracked == BACKTRACK_NO_MATCH) {
|
||||||
|
return NO_MATCH;
|
||||||
|
}
|
||||||
|
if ((backtracked & BACKTRACK_ACCEPT_MASK) != 0) {
|
||||||
|
return (int) backtracked;
|
||||||
|
}
|
||||||
|
state = (int) backtracked;
|
||||||
|
hasSegment = cursor.advance();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSegment = cursor.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long backtrack(RouteSearch.Frame frame,
|
||||||
|
ByteView input,
|
||||||
|
boolean supportsLong,
|
||||||
|
MatchResult<H> out,
|
||||||
|
byte kindLiteral,
|
||||||
|
byte kindMixed,
|
||||||
|
byte kindParam,
|
||||||
|
byte kindWild,
|
||||||
|
byte kindCatch,
|
||||||
|
int len,
|
||||||
|
RouteSearch search,
|
||||||
|
SegmentCursor cursor) {
|
||||||
|
while (search.popInto(frame)) {
|
||||||
|
out.rollbackTo(frame.paramMark);
|
||||||
|
cursor.restore(frame.segStart, frame.segLen, frame.nextIdx);
|
||||||
|
if (frame.kind == kindCatch) {
|
||||||
|
int catchNext = stateCatchAllNext[frame.state];
|
||||||
|
if (catchNext == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
short key = stateCatchAllKeyId[frame.state];
|
||||||
|
if (key >= 0) {
|
||||||
|
out.addParam(key, frame.segStart, len - frame.segStart);
|
||||||
|
}
|
||||||
|
return BACKTRACK_ACCEPT_MASK | (accept(catchNext, out) & 0xffffffffL);
|
||||||
|
}
|
||||||
|
int next;
|
||||||
|
if (frame.kind == kindLiteral) {
|
||||||
|
next = edgeNextState[frame.edgeIndex];
|
||||||
|
} else if (frame.kind == kindMixed) {
|
||||||
|
if (!MixedLookup.match(this, frame.edgeIndex, input, frame.segStart, frame.segLen, supportsLong, out)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
next = edgeNextState[frame.edgeIndex];
|
||||||
|
} else if (frame.kind == kindParam) {
|
||||||
|
out.addParam(stateParamKeyId[frame.state], frame.segStart, frame.segLen);
|
||||||
|
next = stateParamNext[frame.state];
|
||||||
|
} else if (frame.kind == kindWild) {
|
||||||
|
next = edgeNextState[frame.edgeIndex];
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (next >= 0) {
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BACKTRACK_NO_MATCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
int accept(int state, MatchResult<H> out) {
|
||||||
|
int labelId = out.labelId();
|
||||||
|
int start = stateAcceptFirst[state];
|
||||||
|
int count = stateAcceptCount[state];
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int idx = start + i;
|
||||||
|
int acceptLabel = acceptLabelId[idx];
|
||||||
|
if (acceptLabel == 0 || acceptLabel == labelId) {
|
||||||
|
out.setHandler(handlers[acceptHandlerId[idx]]);
|
||||||
|
return acceptRouteId[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NO_MATCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long BACKTRACK_ACCEPT_MASK = 0x4000000000000000L;
|
||||||
|
private static final long BACKTRACK_NO_MATCH = -1L;
|
||||||
|
private static final int NO_MATCH = FastPathRouter.NO_MATCH;
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.fpr.core.MatchResult;
|
||||||
|
import dev.relism.fpr.core.MatchResultAccess;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.lookup.LiteralLookup;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.lookup.MixedLookup;
|
||||||
|
|
||||||
|
final class RouteSearch {
|
||||||
|
static final long NO_CANDIDATE = -1L;
|
||||||
|
|
||||||
|
private int[] stackState;
|
||||||
|
private int[] stackSegStart;
|
||||||
|
private int[] stackSegLen;
|
||||||
|
private int[] stackNextIdx;
|
||||||
|
private int[] stackParamMark;
|
||||||
|
private int[] stackEdgeIndex;
|
||||||
|
private byte[] stackKind;
|
||||||
|
private int[] keyIds;
|
||||||
|
private int[] starts;
|
||||||
|
private int[] lens;
|
||||||
|
private int[] scratchKeyIds;
|
||||||
|
private int[] scratchStarts;
|
||||||
|
private int[] scratchLens;
|
||||||
|
private int[] scratchEdges;
|
||||||
|
private int stackSize;
|
||||||
|
private final Frame frame = new Frame();
|
||||||
|
|
||||||
|
private final byte kindLiteral = (byte) EdgeKind.LITERAL.ordinal();
|
||||||
|
private final byte kindMixed = (byte) EdgeKind.MIXED.ordinal();
|
||||||
|
private final byte kindParam = (byte) EdgeKind.PARAM.ordinal();
|
||||||
|
private final byte kindWild = (byte) EdgeKind.WILD.ordinal();
|
||||||
|
private final byte kindCatch = (byte) EdgeKind.CATCH.ordinal();
|
||||||
|
|
||||||
|
void reset(MatchResult<?> out) {
|
||||||
|
this.stackState = MatchResultAccess.stackState(out);
|
||||||
|
this.stackSegStart = MatchResultAccess.stackSegStart(out);
|
||||||
|
this.stackSegLen = MatchResultAccess.stackSegLen(out);
|
||||||
|
this.stackNextIdx = MatchResultAccess.stackNextIdx(out);
|
||||||
|
this.stackParamMark = MatchResultAccess.stackParamMark(out);
|
||||||
|
this.stackEdgeIndex = MatchResultAccess.stackEdgeIndex(out);
|
||||||
|
this.stackKind = MatchResultAccess.stackKind(out);
|
||||||
|
this.keyIds = MatchResultAccess.keyIds(out);
|
||||||
|
this.starts = MatchResultAccess.starts(out);
|
||||||
|
this.lens = MatchResultAccess.lens(out);
|
||||||
|
this.scratchKeyIds = MatchResultAccess.scratchKeyIds(out);
|
||||||
|
this.scratchStarts = MatchResultAccess.scratchStarts(out);
|
||||||
|
this.scratchLens = MatchResultAccess.scratchLens(out);
|
||||||
|
this.scratchEdges = MatchResultAccess.scratchEdges(out);
|
||||||
|
this.stackSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Frame frame() {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
long selectCandidate(FrozenRouter<?> router,
|
||||||
|
int state,
|
||||||
|
ByteView input,
|
||||||
|
SegmentCursor cursor,
|
||||||
|
boolean supportsLong,
|
||||||
|
MatchResult<?> out) {
|
||||||
|
int segStart = cursor.segStart();
|
||||||
|
int segLen = cursor.segLen();
|
||||||
|
int nextIdx = cursor.nextIdx();
|
||||||
|
int mark = out.mark();
|
||||||
|
|
||||||
|
int literalEdge = LiteralLookup.find(router, input, segStart, segLen, supportsLong, state);
|
||||||
|
|
||||||
|
boolean keepMixedParams = literalEdge == -1;
|
||||||
|
int mixedCount = router.stateMixedCount[state];
|
||||||
|
int mixedStart = router.stateMixedStart[state];
|
||||||
|
int mixedEnd = mixedStart + mixedCount;
|
||||||
|
int mixedMatchCount = 0;
|
||||||
|
int mixedParamCount = 0;
|
||||||
|
if (mixedCount > 0) {
|
||||||
|
int firstByte = input.byteAt(segStart) & 0xFF;
|
||||||
|
long prefixRange = EdgeDispatch.mixedPrefixRange(router, state, firstByte);
|
||||||
|
int rangeStart = EdgeDispatch.rangeStart(prefixRange);
|
||||||
|
int rangeCount = EdgeDispatch.rangeCount(prefixRange);
|
||||||
|
if (rangeCount > 0) {
|
||||||
|
int end = rangeStart + rangeCount;
|
||||||
|
for (int i = rangeStart; i < end; i++) {
|
||||||
|
if (router.edgeLabelLen[i] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (MixedLookup.match(router, i, input, segStart, segLen, supportsLong, out)) {
|
||||||
|
if (mixedMatchCount >= scratchEdges.length) {
|
||||||
|
throw new IllegalStateException("MatchResult stack exhausted");
|
||||||
|
}
|
||||||
|
scratchEdges[mixedMatchCount++] = i;
|
||||||
|
if (mixedMatchCount == 1 && keepMixedParams) {
|
||||||
|
mixedParamCount = out.paramCount() - mark;
|
||||||
|
if (mixedParamCount > 0) {
|
||||||
|
System.arraycopy(keyIds, mark, scratchKeyIds, 0, mixedParamCount);
|
||||||
|
System.arraycopy(starts, mark, scratchStarts, 0, mixedParamCount);
|
||||||
|
System.arraycopy(lens, mark, scratchLens, 0, mixedParamCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.rollbackTo(mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mixedCount > router.stateMixedPrefixCount[state]) {
|
||||||
|
for (int i = mixedStart; i < mixedEnd; i++) {
|
||||||
|
if (router.edgeLabelLen[i] != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (MixedLookup.match(router, i, input, segStart, segLen, supportsLong, out)) {
|
||||||
|
if (mixedMatchCount >= scratchEdges.length) {
|
||||||
|
throw new IllegalStateException("MatchResult stack exhausted");
|
||||||
|
}
|
||||||
|
scratchEdges[mixedMatchCount++] = i;
|
||||||
|
if (mixedMatchCount == 1 && keepMixedParams) {
|
||||||
|
mixedParamCount = out.paramCount() - mark;
|
||||||
|
if (mixedParamCount > 0) {
|
||||||
|
System.arraycopy(keyIds, mark, scratchKeyIds, 0, mixedParamCount);
|
||||||
|
System.arraycopy(starts, mark, scratchStarts, 0, mixedParamCount);
|
||||||
|
System.arraycopy(lens, mark, scratchLens, 0, mixedParamCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.rollbackTo(mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int mixedFirst = mixedMatchCount > 0 ? scratchEdges[0] : -1;
|
||||||
|
int paramNext = router.stateParamNext[state];
|
||||||
|
int wildIndex = router.stateWildIndex[state];
|
||||||
|
int catchNext = router.stateCatchAllNext[state];
|
||||||
|
|
||||||
|
byte kind = -1;
|
||||||
|
int edgeIndex = -1;
|
||||||
|
if (literalEdge != -1) {
|
||||||
|
kind = kindLiteral;
|
||||||
|
edgeIndex = literalEdge;
|
||||||
|
} else if (mixedFirst != -1) {
|
||||||
|
kind = kindMixed;
|
||||||
|
edgeIndex = mixedFirst;
|
||||||
|
} else if (paramNext != -1) {
|
||||||
|
kind = kindParam;
|
||||||
|
} else if (wildIndex != -1) {
|
||||||
|
kind = kindWild;
|
||||||
|
edgeIndex = wildIndex;
|
||||||
|
} else if (catchNext != -1) {
|
||||||
|
kind = kindCatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == -1) {
|
||||||
|
return NO_CANDIDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catchNext != -1 && kind != kindCatch) {
|
||||||
|
push(kindCatch, state, -1, segStart, segLen, nextIdx, mark);
|
||||||
|
}
|
||||||
|
if (wildIndex != -1 && kind != kindWild) {
|
||||||
|
push(kindWild, state, wildIndex, segStart, segLen, nextIdx, mark);
|
||||||
|
}
|
||||||
|
if (paramNext != -1 && kind != kindParam) {
|
||||||
|
push(kindParam, state, -1, segStart, segLen, nextIdx, mark);
|
||||||
|
}
|
||||||
|
if (mixedMatchCount > 0 && (kind != kindMixed || mixedMatchCount > 1)) {
|
||||||
|
for (int i = mixedMatchCount - 1; i >= 0; i--) {
|
||||||
|
if (kind == kindMixed && i == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(kindMixed, state, scratchEdges[i], segStart, segLen, nextIdx, mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == kindMixed) {
|
||||||
|
out.rollbackTo(mark);
|
||||||
|
if (keepMixedParams) {
|
||||||
|
if (mixedParamCount > 0) {
|
||||||
|
System.arraycopy(scratchKeyIds, 0, keyIds, mark, mixedParamCount);
|
||||||
|
System.arraycopy(scratchStarts, 0, starts, mark, mixedParamCount);
|
||||||
|
System.arraycopy(scratchLens, 0, lens, mark, mixedParamCount);
|
||||||
|
}
|
||||||
|
MatchResultAccess.paramCount(out, mark + mixedParamCount);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.rollbackTo(mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pack(kind, edgeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean popInto(Frame out) {
|
||||||
|
if (stackSize == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int idx = --stackSize;
|
||||||
|
out.state = stackState[idx];
|
||||||
|
out.segStart = stackSegStart[idx];
|
||||||
|
out.segLen = stackSegLen[idx];
|
||||||
|
out.nextIdx = stackNextIdx[idx];
|
||||||
|
out.paramMark = stackParamMark[idx];
|
||||||
|
out.edgeIndex = stackEdgeIndex[idx];
|
||||||
|
out.kind = stackKind[idx];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void push(byte kind,
|
||||||
|
int state,
|
||||||
|
int edgeIndex,
|
||||||
|
int segStart,
|
||||||
|
int segLen,
|
||||||
|
int nextIdx,
|
||||||
|
int paramMark) {
|
||||||
|
if (stackSize >= stackState.length) {
|
||||||
|
throw new IllegalStateException("MatchResult stack exhausted");
|
||||||
|
}
|
||||||
|
stackState[stackSize] = state;
|
||||||
|
stackSegStart[stackSize] = segStart;
|
||||||
|
stackSegLen[stackSize] = segLen;
|
||||||
|
stackNextIdx[stackSize] = nextIdx;
|
||||||
|
stackParamMark[stackSize] = paramMark;
|
||||||
|
stackEdgeIndex[stackSize] = edgeIndex;
|
||||||
|
stackKind[stackSize] = kind;
|
||||||
|
stackSize++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long pack(byte kind, int edgeIndex) {
|
||||||
|
return ((long) kind << 32) | (edgeIndex & 0xffffffffL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte kind(long packed) {
|
||||||
|
return (byte) (packed >>> 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int edgeIndex(long packed) {
|
||||||
|
return (int) packed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class Frame {
|
||||||
|
int state;
|
||||||
|
int segStart;
|
||||||
|
int segLen;
|
||||||
|
int nextIdx;
|
||||||
|
int paramMark;
|
||||||
|
int edgeIndex;
|
||||||
|
byte kind;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
|
||||||
|
final class SegmentCursor {
|
||||||
|
private ByteView input;
|
||||||
|
private int len;
|
||||||
|
private int idx;
|
||||||
|
private int segStart;
|
||||||
|
private int segLen;
|
||||||
|
private int nextIdx;
|
||||||
|
|
||||||
|
void reset(ByteView input) {
|
||||||
|
this.input = input;
|
||||||
|
this.len = input.length();
|
||||||
|
this.idx = 0;
|
||||||
|
if (idx < len && input.byteAt(idx) == '/') {
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
this.segStart = 0;
|
||||||
|
this.segLen = -1;
|
||||||
|
this.nextIdx = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean advance() {
|
||||||
|
if (idx >= len) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
segStart = idx;
|
||||||
|
while (idx < len && input.byteAt(idx) != '/') {
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
segLen = idx - segStart;
|
||||||
|
nextIdx = idx;
|
||||||
|
if (idx < len && input.byteAt(idx) == '/') {
|
||||||
|
nextIdx = idx + 1;
|
||||||
|
}
|
||||||
|
idx = nextIdx;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void restore(int segStart, int segLen, int nextIdx) {
|
||||||
|
this.segStart = segStart;
|
||||||
|
this.segLen = segLen;
|
||||||
|
this.nextIdx = nextIdx;
|
||||||
|
this.idx = nextIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
int segStart() {
|
||||||
|
return segStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
int segLen() {
|
||||||
|
return segLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextIdx() {
|
||||||
|
return nextIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
int length() {
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
}
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime.lookup;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.ByteCompare;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.FrozenRouter;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup strategies for literal edges, selected per state at compile-time.
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public final class LiteralLookup {
|
||||||
|
public static int find(FrozenRouter<?> router,
|
||||||
|
ByteView input,
|
||||||
|
int segStart,
|
||||||
|
int segLen,
|
||||||
|
boolean supportsLong,
|
||||||
|
int state) {
|
||||||
|
int count = router.stateLiteralCount[state];
|
||||||
|
if (count == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int start = router.stateLiteralStart[state];
|
||||||
|
byte strategy = router.stateLiteralStrategy[state];
|
||||||
|
if (strategy == LiteralLookupStrategy.LINEAR) {
|
||||||
|
return linear(router, input, segStart, segLen, supportsLong, start, count);
|
||||||
|
}
|
||||||
|
if (strategy == LiteralLookupStrategy.ORDERED_PREFIX) {
|
||||||
|
return orderedPrefix(router, input, segStart, segLen, supportsLong, start, count);
|
||||||
|
}
|
||||||
|
if (strategy == LiteralLookupStrategy.HASH) {
|
||||||
|
return hash(router, input, segStart, segLen, supportsLong, state, start, count);
|
||||||
|
}
|
||||||
|
return linear(router, input, segStart, segLen, supportsLong, start, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int linear(FrozenRouter<?> router,
|
||||||
|
ByteView input,
|
||||||
|
int segStart,
|
||||||
|
int segLen,
|
||||||
|
boolean supportsLong,
|
||||||
|
int start,
|
||||||
|
int count) {
|
||||||
|
int end = start + count;
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
if (router.edgeLabelLen[i] != segLen) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ByteCompare.equals(input, segStart, router.blob, router.edgeLabelOff[i], segLen, supportsLong)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int orderedPrefix(FrozenRouter<?> router,
|
||||||
|
ByteView input,
|
||||||
|
int segStart,
|
||||||
|
int segLen,
|
||||||
|
boolean supportsLong,
|
||||||
|
int start,
|
||||||
|
int count) {
|
||||||
|
long key = prefixKey(input, segStart, segLen, supportsLong);
|
||||||
|
int lo = start;
|
||||||
|
int hi = start + count - 1;
|
||||||
|
while (lo <= hi) {
|
||||||
|
int mid = (lo + hi) >>> 1;
|
||||||
|
long midKey = router.edgeLiteralPrefix[mid];
|
||||||
|
int cmp = compare(segLen, key, router.edgeLabelLen[mid], midKey);
|
||||||
|
if (cmp < 0) {
|
||||||
|
hi = mid - 1;
|
||||||
|
} else if (cmp > 0) {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else {
|
||||||
|
int left = mid;
|
||||||
|
while (left > start && compare(segLen, key, router.edgeLabelLen[left - 1], router.edgeLiteralPrefix[left - 1]) == 0) {
|
||||||
|
left--;
|
||||||
|
}
|
||||||
|
int right = mid;
|
||||||
|
int end = start + count;
|
||||||
|
while (right + 1 < end && compare(segLen, key, router.edgeLabelLen[right + 1], router.edgeLiteralPrefix[right + 1]) == 0) {
|
||||||
|
right++;
|
||||||
|
}
|
||||||
|
for (int i = left; i <= right; i++) {
|
||||||
|
if (router.edgeLabelLen[i] != segLen) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ByteCompare.equals(input, segStart, router.blob, router.edgeLabelOff[i], segLen, supportsLong)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int hash(FrozenRouter<?> router,
|
||||||
|
ByteView input,
|
||||||
|
int segStart,
|
||||||
|
int segLen,
|
||||||
|
boolean supportsLong,
|
||||||
|
int state,
|
||||||
|
int start,
|
||||||
|
int count) {
|
||||||
|
int mask = router.stateLiteralHashMask[state];
|
||||||
|
int off = router.stateLiteralHashOff[state];
|
||||||
|
if (mask <= 0 || off < 0) {
|
||||||
|
return linear(router, input, segStart, segLen, supportsLong, start, count);
|
||||||
|
}
|
||||||
|
long key = prefixKey(input, segStart, segLen, supportsLong);
|
||||||
|
int slot = mix(key) & mask;
|
||||||
|
while (true) {
|
||||||
|
long stored = router.literalHashKey[off + slot];
|
||||||
|
if (stored == 0L) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (stored == key) {
|
||||||
|
int edgeIndex = router.literalHashEdge[off + slot];
|
||||||
|
if (edgeIndex >= start && edgeIndex < start + count && router.edgeLabelLen[edgeIndex] == segLen
|
||||||
|
&& ByteCompare.equals(input, segStart, router.blob, router.edgeLabelOff[edgeIndex], segLen, supportsLong)) {
|
||||||
|
return edgeIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slot = (slot + 1) & mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int compare(int aLen, long aKey, short bLen, long bKey) {
|
||||||
|
if (aLen != bLen) {
|
||||||
|
return Integer.compare(aLen, bLen);
|
||||||
|
}
|
||||||
|
return Long.compareUnsigned(aKey, bKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long prefixKey(ByteView input,
|
||||||
|
int segStart,
|
||||||
|
int segLen,
|
||||||
|
boolean supportsLong) {
|
||||||
|
if (segLen >= 8 && supportsLong && segStart + 8 <= input.length()) {
|
||||||
|
return input.longAt(segStart);
|
||||||
|
}
|
||||||
|
long key = 0;
|
||||||
|
int len = Math.min(8, segLen);
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
key |= ((long) input.byteAt(segStart + i) & 0xFFL) << (i * 8);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int mix(long key) {
|
||||||
|
long h = key ^ (key >>> 33);
|
||||||
|
h *= 0xff51afd7ed558ccdL;
|
||||||
|
h ^= (h >>> 33);
|
||||||
|
h *= 0xc4ceb9fe1a85ec53L;
|
||||||
|
h ^= (h >>> 33);
|
||||||
|
return (int) h;
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime.lookup;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy ids for literal edge lookup, selected at compile time.
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public final class LiteralLookupStrategy {
|
||||||
|
public static final byte LINEAR = 0;
|
||||||
|
public static final byte ORDERED_PREFIX = 1;
|
||||||
|
public static final byte HASH = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime.lookup;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.fpr.core.MatchResult;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.ByteCompare;
|
||||||
|
import dev.relism.fpr.core.internal.runtime.FrozenRouter;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matching strategies for mixed literal/param segments.
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public final class MixedLookup {
|
||||||
|
public static boolean match(FrozenRouter<?> router,
|
||||||
|
int edgeIndex,
|
||||||
|
ByteView input,
|
||||||
|
int segStart,
|
||||||
|
int segLen,
|
||||||
|
boolean supportsLong,
|
||||||
|
MatchResult<?> out) {
|
||||||
|
byte strategy = router.edgeMixedStrategy[edgeIndex];
|
||||||
|
if (strategy == MixedLookupStrategy.ONE) {
|
||||||
|
return matchOne(router, edgeIndex, input, segStart, segLen, supportsLong, out);
|
||||||
|
}
|
||||||
|
return matchGeneral(router, edgeIndex, input, segStart, segLen, supportsLong, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchOne(FrozenRouter<?> router,
|
||||||
|
int edgeIndex,
|
||||||
|
ByteView input,
|
||||||
|
int segStart,
|
||||||
|
int segLen,
|
||||||
|
boolean supportsLong,
|
||||||
|
MatchResult<?> out) {
|
||||||
|
int chunkOff = router.edgeMixedChunkOff[edgeIndex];
|
||||||
|
int paramOff = router.edgeMixedParamOff[edgeIndex];
|
||||||
|
int lit0Off = router.mixedChunkOff[chunkOff];
|
||||||
|
int lit0Len = router.mixedChunkLen[chunkOff];
|
||||||
|
int lit1Off = router.mixedChunkOff[chunkOff + 1];
|
||||||
|
int lit1Len = router.mixedChunkLen[chunkOff + 1];
|
||||||
|
int required = lit0Len + lit1Len;
|
||||||
|
int paramLen = segLen - required;
|
||||||
|
if (paramLen <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ByteCompare.equals(input, segStart, router.blob, lit0Off, lit0Len, supportsLong)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int suffixStart = segStart + segLen - lit1Len;
|
||||||
|
if (!ByteCompare.equals(input, suffixStart, router.blob, lit1Off, lit1Len, supportsLong)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int paramStart = segStart + lit0Len;
|
||||||
|
out.addParam(router.mixedParamKeyId[paramOff], paramStart, paramLen);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchGeneral(FrozenRouter<?> router,
|
||||||
|
int edgeIndex,
|
||||||
|
ByteView input,
|
||||||
|
int segStart,
|
||||||
|
int segLen,
|
||||||
|
boolean supportsLong,
|
||||||
|
MatchResult<?> out) {
|
||||||
|
int chunkOff = router.edgeMixedChunkOff[edgeIndex];
|
||||||
|
int chunkCount = router.edgeMixedChunkCount[edgeIndex];
|
||||||
|
int paramOff = router.edgeMixedParamOff[edgeIndex];
|
||||||
|
int paramCount = router.edgeMixedParamCount[edgeIndex];
|
||||||
|
int end = segStart + segLen;
|
||||||
|
int cursor = segStart;
|
||||||
|
|
||||||
|
for (int i = 0; i < paramCount; i++) {
|
||||||
|
int litOff = router.mixedChunkOff[chunkOff + i];
|
||||||
|
int litLen = router.mixedChunkLen[chunkOff + i];
|
||||||
|
if (!ByteCompare.equals(input, cursor, router.blob, litOff, litLen, supportsLong)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cursor += litLen;
|
||||||
|
int nextLitOff = router.mixedChunkOff[chunkOff + i + 1];
|
||||||
|
int nextLitLen = router.mixedChunkLen[chunkOff + i + 1];
|
||||||
|
int nextPos;
|
||||||
|
if (nextLitLen == 0) {
|
||||||
|
nextPos = end;
|
||||||
|
} else {
|
||||||
|
nextPos = ByteCompare.indexOf(input, cursor, end - nextLitLen, router.blob, nextLitOff, nextLitLen, supportsLong);
|
||||||
|
if (nextPos < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int paramLen = nextPos - cursor;
|
||||||
|
if (paramLen <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.addParam(router.mixedParamKeyId[paramOff + i], cursor, paramLen);
|
||||||
|
cursor = nextPos;
|
||||||
|
}
|
||||||
|
int tailOff = router.mixedChunkOff[chunkOff + chunkCount - 1];
|
||||||
|
int tailLen = router.mixedChunkLen[chunkOff + chunkCount - 1];
|
||||||
|
return ByteCompare.equals(input, cursor, router.blob, tailOff, tailLen, supportsLong);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.relism.fpr.core.internal.runtime.lookup;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy ids for mixed-segment matching, selected at compile time.
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public final class MixedLookupStrategy {
|
||||||
|
public static final byte ONE = 0;
|
||||||
|
public static final byte TWO = 1;
|
||||||
|
public static final byte N = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package dev.relism.fpr.core;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.dsl.StringRouteParser;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class RouterConcurrencyTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConcurrentMatchingDoesNotCorruptState() throws InterruptedException {
|
||||||
|
int threadCount = 20;
|
||||||
|
int iterationsPerThread = 100_000;
|
||||||
|
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/users"), "USERS_LIST");
|
||||||
|
builder.add(StringRouteParser.parse("/users/{id}"), "USER_DETAIL");
|
||||||
|
builder.add(StringRouteParser.parse("/users/{id}/orders/{orderId}"), "USER_ORDER");
|
||||||
|
builder.add(StringRouteParser.parse("/static/pre-{x}-suf"), "STATIC_MIXED");
|
||||||
|
builder.add(StringRouteParser.parse("/assets/**"), "ASSETS_CATCH_ALL");
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
|
||||||
|
byte[] path1 = "/users".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
byte[] path2 = "/users/123".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
byte[] path3 = "/users/123/orders/abc".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
byte[] path4 = "/static/pre-xyz-suf".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
byte[] path5 = "/assets/css/style.css".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
byte[] path6 = "/not-found".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
|
||||||
|
ByteView[] views = new ByteView[] {
|
||||||
|
new ByteArrayView(path1),
|
||||||
|
new ByteArrayView(path2),
|
||||||
|
new ByteArrayView(path3),
|
||||||
|
new ByteArrayView(path4),
|
||||||
|
new ByteArrayView(path5),
|
||||||
|
new ByteArrayView(path6)
|
||||||
|
};
|
||||||
|
|
||||||
|
String[] expectedHandlers = new String[] {
|
||||||
|
"USERS_LIST",
|
||||||
|
"USER_DETAIL",
|
||||||
|
"USER_ORDER",
|
||||||
|
"STATIC_MIXED",
|
||||||
|
"ASSETS_CATCH_ALL",
|
||||||
|
null
|
||||||
|
};
|
||||||
|
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||||
|
CountDownLatch startLatch = new CountDownLatch(1);
|
||||||
|
CountDownLatch endLatch = new CountDownLatch(threadCount);
|
||||||
|
|
||||||
|
List<Future<Integer>> futures = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < threadCount; i++) {
|
||||||
|
final int threadIndex = i;
|
||||||
|
futures.add(executor.submit((Callable<Integer>) () -> {
|
||||||
|
MatchResult<String> result = new MatchResult<>(builder.maxParamCount(), 64);
|
||||||
|
int localFailures = 0;
|
||||||
|
|
||||||
|
startLatch.await(); // wait for all threads to be ready
|
||||||
|
|
||||||
|
for (int j = 0; j < iterationsPerThread; j++) {
|
||||||
|
int pathIndex = (j + threadIndex) % views.length;
|
||||||
|
ByteView view = views[pathIndex];
|
||||||
|
String expectedHandler = expectedHandlers[pathIndex];
|
||||||
|
|
||||||
|
result.reset();
|
||||||
|
int routeId = router.match(view, result);
|
||||||
|
|
||||||
|
if (expectedHandler == null) {
|
||||||
|
if (routeId != FastPathRouter.NO_MATCH || result.handler() != null) {
|
||||||
|
localFailures++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (routeId == FastPathRouter.NO_MATCH || !expectedHandler.equals(result.handler())) {
|
||||||
|
localFailures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endLatch.countDown();
|
||||||
|
return localFailures;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire!
|
||||||
|
startLatch.countDown();
|
||||||
|
boolean completed = endLatch.await(30, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertTrue(completed, "Concurrency test timed out");
|
||||||
|
|
||||||
|
int totalFailures = 0;
|
||||||
|
for (Future<Integer> future : futures) {
|
||||||
|
try {
|
||||||
|
totalFailures += future.get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
totalFailures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.shutdownNow();
|
||||||
|
|
||||||
|
// 0 failures means every matching attempt returned the correct route
|
||||||
|
assertEquals(0, totalFailures, "There were mismatched routes during concurrent access (race condition)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ByteArrayView implements ByteView {
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
private ByteArrayView(byte[] bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int length() {
|
||||||
|
return bytes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte byteAt(int index) {
|
||||||
|
return bytes[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsLong() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long longAt(int index) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package dev.relism.fpr.core;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.dsl.StringRouteParser;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.VarHandle;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
class RouterMatchTest {
|
||||||
|
@Test
|
||||||
|
void routePrecedencePrefersLiteralMixedParamWildcardCatchAll() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/a/b"), "LITERAL");
|
||||||
|
builder.add(StringRouteParser.parse("/a/pre-{x}-suf"), "MIXED");
|
||||||
|
builder.add(StringRouteParser.parse("/a/{id}"), "PARAM");
|
||||||
|
builder.add(StringRouteParser.parse("/a/*"), "WILD");
|
||||||
|
builder.add(StringRouteParser.parse("/a/**"), "CATCH");
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
|
||||||
|
assertThat(router.match(view("/a/b"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("LITERAL");
|
||||||
|
|
||||||
|
assertThat(router.match(view("/a/pre-123-suf"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("MIXED");
|
||||||
|
|
||||||
|
assertThat(router.match(view("/a/zzz"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("PARAM");
|
||||||
|
|
||||||
|
assertThat(router.match(view("/a/zzz/extra"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("CATCH");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mixedSegmentsCaptureParams() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/some{id}"), "A");
|
||||||
|
builder.add(StringRouteParser.parse("/pre-{x}-suf"), "B");
|
||||||
|
builder.add(RoutePattern.of(RoutePattern.mixed(new String[]{"", "-suf"}, new String[]{"x"})), "C");
|
||||||
|
builder.add(RoutePattern.of(RoutePattern.mixed(new String[]{"pre-", ""}, new String[]{"x"})), "D");
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
|
||||||
|
router.match(view("/some123"), out);
|
||||||
|
assertThat(out.paramCount()).isEqualTo(1);
|
||||||
|
assertThat(span(view("/some123"), out, 0)).isEqualTo("123");
|
||||||
|
|
||||||
|
router.match(view("/pre-abc-suf"), out);
|
||||||
|
assertThat(out.paramCount()).isEqualTo(1);
|
||||||
|
assertThat(span(view("/pre-abc-suf"), out, 0)).isEqualTo("abc");
|
||||||
|
|
||||||
|
router.match(view("/123-suf"), out);
|
||||||
|
assertThat(out.paramCount()).isEqualTo(1);
|
||||||
|
assertThat(out.handler()).isEqualTo("C");
|
||||||
|
assertThat(span(view("/123-suf"), out, 0)).isEqualTo("123");
|
||||||
|
|
||||||
|
router.match(view("/pre-123"), out);
|
||||||
|
assertThat(out.paramCount()).isEqualTo(1);
|
||||||
|
assertThat(out.handler()).isEqualTo("D");
|
||||||
|
assertThat(span(view("/pre-123"), out, 0)).isEqualTo("123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mixedEmptyPrefixStillMatches() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(RoutePattern.of(RoutePattern.mixed(new String[]{"", "-verylongsuffix"}, new String[]{"x"})), "A");
|
||||||
|
builder.add(StringRouteParser.parse("/b{y}"), "B");
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
|
||||||
|
assertThat(router.match(view("/abc-verylongsuffix"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("A");
|
||||||
|
assertThat(span(view("/abc-verylongsuffix"), out, 0)).isEqualTo("abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fallbackToLowerPrecedenceWhenHigherPathFails() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/a/b/x"), "LIT");
|
||||||
|
builder.add(StringRouteParser.parse("/a/{id}/y"), "PARAM");
|
||||||
|
builder.add(StringRouteParser.parse("/a/**"), "CATCH");
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount(), 64);
|
||||||
|
|
||||||
|
router.match(view("/a/b/y"), out);
|
||||||
|
assertThat(out.handler()).isEqualTo("PARAM");
|
||||||
|
|
||||||
|
router.match(view("/a/123/z"), out);
|
||||||
|
assertThat(out.handler()).isEqualTo("CATCH");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indexedLiteralLookupMatchesAcrossStates() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
builder.add(StringRouteParser.parse("/s0/r" + i), "S0-" + i);
|
||||||
|
builder.add(StringRouteParser.parse("/s1/r" + i), "S1-" + i);
|
||||||
|
builder.add(StringRouteParser.parse("/s2/r" + i), "S2-" + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount(), 64);
|
||||||
|
|
||||||
|
assertThat(router.match(view("/s0/r19"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("S0-19");
|
||||||
|
|
||||||
|
assertThat(router.match(view("/s1/r7"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("S1-7");
|
||||||
|
|
||||||
|
assertThat(router.match(view("/s2/r3"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("S2-3");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchDoesNotReplaceResultArrays() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/a/{id}"), "A");
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
int[] keyIds = out.keyIdsArray();
|
||||||
|
int[] starts = out.startsArray();
|
||||||
|
int[] lens = out.lensArray();
|
||||||
|
|
||||||
|
router.match(view("/a/123"), out);
|
||||||
|
|
||||||
|
assertThat(out.keyIdsArray()).isSameAs(keyIds);
|
||||||
|
assertThat(out.startsArray()).isSameAs(starts);
|
||||||
|
assertThat(out.lensArray()).isSameAs(lens);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void forEachParamProvidesNamesAndSpans() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/users/{id}/orders/{orderId}"), "A");
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
ByteView view = view("/users/42/orders/7");
|
||||||
|
|
||||||
|
assertThat(router.match(view, out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
|
||||||
|
String[] paramNames = builder.paramNames();
|
||||||
|
List<String> seen = new ArrayList<>();
|
||||||
|
out.forEachParam(view, paramNames, (name, bytes, start, len) -> {
|
||||||
|
seen.add(name + "=" + span(bytes, start, len));
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThat(seen).containsExactly("id=42", "orderId=7");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void paramNamesAreOrderedAndUnique() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/a/{id}"), "A");
|
||||||
|
builder.add(StringRouteParser.parse("/b/{id}/c/{slug}"), "B");
|
||||||
|
builder.add(StringRouteParser.parse("/c/{slug}/d/{id}"), "C");
|
||||||
|
|
||||||
|
assertThat(builder.paramNames()).containsExactly("id", "slug");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void forEachParamRejectsMissingNameEntries() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/users/{id}/orders/{orderId}"), "A");
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
ByteView view = view("/users/42/orders/7");
|
||||||
|
router.match(view, out);
|
||||||
|
|
||||||
|
String[] paramNames = new String[]{"id"};
|
||||||
|
assertThatThrownBy(() -> out.forEachParam(view, paramNames, (name, bytes, start, len) -> {
|
||||||
|
})).isInstanceOf(IllegalArgumentException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void labelsDifferentiateSamePath() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add("GET", StringRouteParser.parse("/users"), "GET_HANDLER");
|
||||||
|
builder.add("POST", StringRouteParser.parse("/users"), "POST_HANDLER");
|
||||||
|
|
||||||
|
int getId = builder.labelId("GET");
|
||||||
|
int postId = builder.labelId("POST");
|
||||||
|
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
|
||||||
|
out.labelId(getId);
|
||||||
|
assertThat(router.match(view("/users"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("GET_HANDLER");
|
||||||
|
|
||||||
|
out.labelId(postId);
|
||||||
|
assertThat(router.match(view("/users"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("POST_HANDLER");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void labelZeroMatchesAny() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add("GET", StringRouteParser.parse("/assets"), "GET_ASSETS");
|
||||||
|
builder.add(StringRouteParser.parse("/assets"), "ANY_ASSETS");
|
||||||
|
|
||||||
|
int getId = builder.labelId("GET");
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
|
||||||
|
out.labelId(getId);
|
||||||
|
assertThat(router.match(view("/assets"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("GET_ASSETS");
|
||||||
|
|
||||||
|
out.labelId(0);
|
||||||
|
assertThat(router.match(view("/assets"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("ANY_ASSETS");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ByteView view(String path) {
|
||||||
|
return new ByteArrayView(path.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String span(ByteView view, MatchResult<String> out, int index) {
|
||||||
|
int start = out.startAt(index);
|
||||||
|
int len = out.lenAt(index);
|
||||||
|
byte[] bytes = new byte[len];
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
bytes[i] = view.byteAt(start + i);
|
||||||
|
}
|
||||||
|
return new String(bytes, StandardCharsets.US_ASCII);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String span(ByteView view, int start, int len) {
|
||||||
|
byte[] bytes = new byte[len];
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
bytes[i] = view.byteAt(start + i);
|
||||||
|
}
|
||||||
|
return new String(bytes, StandardCharsets.US_ASCII);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ByteArrayView implements ByteView {
|
||||||
|
private static final VarHandle LONG_VIEW = MethodHandles.byteArrayViewVarHandle(long[].class, java.nio.ByteOrder.LITTLE_ENDIAN);
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
private ByteArrayView(byte[] bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int length() {
|
||||||
|
return bytes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte byteAt(int index) {
|
||||||
|
return bytes[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsLong() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long longAt(int index) {
|
||||||
|
return (long) LONG_VIEW.get(bytes, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fastpathrouter-parent</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>fpr-netty</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-bom</artifactId>
|
||||||
|
<version>${netty.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fpr-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-buffer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-codec-http</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package dev.relism.fpr.netty;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
|
||||||
|
public final class NettyByteBufView implements ByteView {
|
||||||
|
private final ByteBuf buffer;
|
||||||
|
private final int offset;
|
||||||
|
private final int length;
|
||||||
|
|
||||||
|
public NettyByteBufView(ByteBuf buffer, int offset, int length) {
|
||||||
|
if (buffer == null) {
|
||||||
|
throw new IllegalArgumentException("buffer must not be null");
|
||||||
|
}
|
||||||
|
if (offset < 0 || length < 0 || offset + length > buffer.capacity()) {
|
||||||
|
throw new IllegalArgumentException("invalid offset/length");
|
||||||
|
}
|
||||||
|
this.buffer = buffer;
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuf buffer() {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int offset() {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int length() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte byteAt(int index) {
|
||||||
|
if (index < 0 || index >= length) {
|
||||||
|
throw new IndexOutOfBoundsException("index: " + index);
|
||||||
|
}
|
||||||
|
return buffer.getByte(offset + index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsLong() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long longAt(int index) {
|
||||||
|
if (index < 0 || index + 8 > length) {
|
||||||
|
throw new IndexOutOfBoundsException("index: " + index);
|
||||||
|
}
|
||||||
|
return buffer.getLongLE(offset + index);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package dev.relism.fpr.netty;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
|
||||||
|
public final class NettyPathExtractor {
|
||||||
|
private NettyPathExtractor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PathSpan extractFromRequestLine(ByteBuf buffer, int offset, int length, PathSpan out) {
|
||||||
|
if (out == null) {
|
||||||
|
throw new IllegalArgumentException("out must not be null");
|
||||||
|
}
|
||||||
|
int end = offset + length;
|
||||||
|
int i = offset;
|
||||||
|
while (i < end && buffer.getByte(i) != ' ') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i >= end) {
|
||||||
|
throw new IllegalArgumentException("invalid request line");
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
int pathStart = i;
|
||||||
|
while (i < end && buffer.getByte(i) != ' ') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
int pathLen = i - pathStart;
|
||||||
|
out.set(pathStart, pathLen);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PathSpan extractFromUri(CharSequence uri, PathSpan out) {
|
||||||
|
if (out == null) {
|
||||||
|
throw new IllegalArgumentException("out must not be null");
|
||||||
|
}
|
||||||
|
int len = uri.length();
|
||||||
|
int end = len;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
if (uri.charAt(i) == '?') {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.set(0, end);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class PathSpan {
|
||||||
|
private int start;
|
||||||
|
private int len;
|
||||||
|
|
||||||
|
public int start() {
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int len() {
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(int start, int len) {
|
||||||
|
this.start = start;
|
||||||
|
this.len = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package dev.relism.fpr.netty;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.fpr.core.FastPathRouter;
|
||||||
|
import dev.relism.fpr.core.MatchResult;
|
||||||
|
import dev.relism.fpr.core.RouterBuilder;
|
||||||
|
import dev.relism.fpr.core.dsl.StringRouteParser;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class NettyAdaptersTest {
|
||||||
|
@Test
|
||||||
|
void byteBufViewRespectsOffsetAndLength() {
|
||||||
|
ByteBuf buf = Unpooled.wrappedBuffer("abcdef".getBytes(StandardCharsets.US_ASCII));
|
||||||
|
NettyByteBufView view = new NettyByteBufView(buf, 1, 3);
|
||||||
|
|
||||||
|
assertThat(view.length()).isEqualTo(3);
|
||||||
|
assertThat((char) view.byteAt(0)).isEqualTo('b');
|
||||||
|
assertThat((char) view.byteAt(2)).isEqualTo('d');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractPathFromRequestLine() {
|
||||||
|
ByteBuf buf = Unpooled.wrappedBuffer("GET /alpha/beta?q=1 HTTP/1.1".getBytes(StandardCharsets.US_ASCII));
|
||||||
|
NettyPathExtractor.PathSpan out = new NettyPathExtractor.PathSpan();
|
||||||
|
|
||||||
|
NettyPathExtractor.extractFromRequestLine(buf, 0, buf.readableBytes(), out);
|
||||||
|
|
||||||
|
String path = buf.toString(out.start(), out.len(), StandardCharsets.US_ASCII);
|
||||||
|
assertThat(path).isEqualTo("/alpha/beta?q=1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nettyIntegrationMatch() {
|
||||||
|
RouterBuilder<String> builder = new RouterBuilder<>();
|
||||||
|
builder.add(StringRouteParser.parse("/user/{id}"), "USER");
|
||||||
|
FastPathRouter<ByteView, String> router = builder.compile();
|
||||||
|
|
||||||
|
ByteBuf buf = Unpooled.wrappedBuffer("/user/42".getBytes(StandardCharsets.US_ASCII));
|
||||||
|
NettyByteBufView view = new NettyByteBufView(buf, 0, buf.readableBytes());
|
||||||
|
|
||||||
|
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
|
||||||
|
assertThat(router.match(view, out)).isNotEqualTo(FastPathRouter.NO_MATCH);
|
||||||
|
assertThat(out.handler()).isEqualTo("USER");
|
||||||
|
assertThat(out.paramCount()).isEqualTo(1);
|
||||||
|
assertThat(span(view, out, 0)).isEqualTo("42");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String span(ByteView view, MatchResult<String> out, int index) {
|
||||||
|
int start = out.startAt(index);
|
||||||
|
int len = out.lenAt(index);
|
||||||
|
byte[] bytes = new byte[len];
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
bytes[i] = view.byteAt(start + i);
|
||||||
|
}
|
||||||
|
return new String(bytes, StandardCharsets.US_ASCII);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fastpathrouter-parent</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>fpr-core</module>
|
||||||
|
<module>fpr-netty</module>
|
||||||
|
<module>fpr-bench</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.release>11</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
||||||
|
<netty.version>4.2.9.Final</netty.version>
|
||||||
|
<lombok.version>1.18.42</lombok.version>
|
||||||
|
<slf4j.version>2.0.17</slf4j.version>
|
||||||
|
<junit.version>5.14.2</junit.version>
|
||||||
|
<assertj.version>3.27.6</assertj.version>
|
||||||
|
<jmh.version>1.37</jmh.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<distributionManagement>
|
||||||
|
<repository>
|
||||||
|
<id>Personal</id>
|
||||||
|
<url>https://maven.relism.dev/releases</url>
|
||||||
|
</repository>
|
||||||
|
</distributionManagement>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fpr-core</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fpr-netty</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>${slf4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>${assertj.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<version>${slf4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-core</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>${maven.compiler.release}</release>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.5.2</version>
|
||||||
|
<configuration>
|
||||||
|
<useModulePath>false</useModulePath>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
Reference in New Issue
Block a user