Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incremental loading #29

Merged
merged 10 commits into from
Dec 29, 2023
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
### 0.2.10-SNAPSHOT (TBD)

#### Features
* Added `IncrementalLoading` to the routing module. Checkout the routing documentation for more details.

#### Improvements
* Simplify the authorization process for a given OAuth2 authentication provider via the `jpro-auth-routing` module
by calling the `OAuth2Filter.authorize` method.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,8 @@ dependencies {
./gradlew jpro-media:example:run -Psample=media-player
./gradlew jpro-media:example:run -Psample=media-recorder
./gradlew jpro-media:example:run -Psample=media-recorder-and-player
./gradlew jpro-routing:example:run -Psample=colors
./gradlew jpro-routing:example:run -Psample=test
```

- As JPro application
Expand All @@ -482,4 +484,6 @@ dependencies {
./gradlew jpro-media:example:jproRun -Psample=media-player
./gradlew jpro-media:example:jproRun -Psample=media-recorder
./gradlew jpro-media:example:jproRun -Psample=media-recorder-and-player
./gradlew jpro-routing:example:jproRun -Psample=colors
./gradlew jpro-routing:example:jproRun -Psample=test
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
import javafx.application.Platform;

import java.time.Duration;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

public class FreezeDetector {

Thread fxthread;
Long lastUpdate;
Integer counter = 0;

public FreezeDetector() {
this(Duration.ofSeconds(1));
}

public FreezeDetector(Duration duration, Consumer<Thread> callback) {
public FreezeDetector(Duration duration, BiConsumer<Thread, Duration> callback) {
if(!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Can run only on the FX thread");
}
Expand All @@ -26,29 +28,28 @@ public FreezeDetector(Duration duration, Consumer<Thread> callback) {
@Override
public void handle(long now) {
lastUpdate = System.currentTimeMillis();
counter = 1;
}
}.start();
lastUpdate = System.currentTimeMillis();
counter = 1;

var t = new Thread(() -> {
while(fxthread.getState() != Thread.State.TERMINATED) {
long now = System.currentTimeMillis();
long timeGone = now - lastUpdate;
try {
long toSleep = duration.toMillis() - timeGone;
long toSleep = (duration.toMillis() * counter - timeGone);
if(toSleep > 0) {
Thread.sleep(toSleep);
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
if(System.currentTimeMillis() - lastUpdate > duration.toMillis()) {
callback.accept(fxthread);
lastUpdate = System.currentTimeMillis();
try {
Thread.sleep(duration.toMillis());
} catch (InterruptedException ex) {
ex.printStackTrace();
}
long timeGone2 = System.currentTimeMillis() - lastUpdate;
if(timeGone2 > duration.toMillis() * counter) {
counter += 1;
callback.accept(fxthread, Duration.ofMillis(timeGone2));
}
}
}, "FX-Freeze-Detector-Thread");
Expand All @@ -57,8 +58,8 @@ public void handle(long now) {
}

public FreezeDetector(Duration duration) {
this(duration, (thread) -> {
System.out.println("Freeze detected");
this(duration, (thread, timeGone) -> {
System.out.println("Freeze detected for " + timeGone.toMillis() + "ms");
System.out.println(" Thread: " + thread.getName());
for (StackTraceElement element : thread.getStackTrace()) {
System.out.println(" " + element);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ public static void startJavaFX() throws InterruptedException {

@Test
public void testFreezeDetector() throws InterruptedException {
// THIS IS UNSTABLE WITH MAC AND JAVAFX < 21
AtomicInteger counter = new AtomicInteger(0);
inFX(() -> new FreezeDetector(Duration.ofMillis(100),
thread -> counter.incrementAndGet()));
(thread,duration) -> {
counter.incrementAndGet();
}));

assertEquals(0, counter.get());
Thread.sleep(200);
Expand All @@ -50,7 +53,7 @@ public void testFreezeDetector() throws InterruptedException {
assertEquals(2, counter.get());
}

public void inFX(Runnable r) {
public static void inFX(Runnable r) {
CountDownLatch l = new CountDownLatch(1);
AtomicReference<Throwable> ex = new AtomicReference<>();
Platform.runLater(() -> {
Expand Down
20 changes: 20 additions & 0 deletions jpro-routing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,23 @@ This improves the performance and user experience and ensures the same behavior

Currently, it's required to set a resource called `jpro/html/defaultpage` in the resources -
checkout our sample project: https://github.com/JPro-one/jpro-routing-sample



### Additional Features

#### Incremental Loading

It's possible to load parts of the application incrementally.
This is especially useful when optimizing the start time for websites.

```
import one.jpro.platform.routing.performance.IncrementalLoading;

...
parent.getChildren().add(IncrementalLoading.loadNode(yourNode));
...
```

When this is done - JPro sends one Node at a time to the client.
This allows the client to render the frame as soon as possible - ensuring early visible content for the user.
22 changes: 11 additions & 11 deletions jpro-routing/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,6 @@ configure(project(':jpro-routing:example')) {
apply plugin: 'org.openjfx.javafxplugin'
apply plugin: 'jpro-gradle-plugin'

//mainClassName = "example.scala.TestWebApplication"
//mainClassName = "example.scala.TestExtensions"
//mainClassName = "example.scala.ColorTransitionApp"
mainClassName = "example.colors.ColorsApp"
//mainClassName = "example.popup.PopupApp"

application {
mainClass = "$mainClassName"
mainModule = moduleName
}

dependencies {
implementation project(':jpro-routing:core')
implementation project(':jpro-routing:dev')
Expand All @@ -114,4 +103,15 @@ configure(project(':jpro-routing:example')) {
jpro {
openingPath = "/"
}

def examples = [
'colors' : 'example.colors.ColorsApp',
'test' : 'example.scala.TestWebApplication'
]
mainClassName = project.hasProperty("sample") ? examples[project.getProperties().get("sample")] : examples["colors"]

application {
mainClass = "$mainClassName"
mainModule = moduleName
}
}
1 change: 1 addition & 0 deletions jpro-routing/core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

exports one.jpro.platform.routing;
exports one.jpro.platform.routing.crawl;
exports one.jpro.platform.routing.performance;
exports one.jpro.platform.routing.filter.container;
exports one.jpro.platform.routing.sessionmanager;
exports one.jpro.platform.routing.extensions.linkheader;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package one.jpro.platform.routing.performance;

/**
* I only exist to allow the creation of a module-info.java file.
*/
public class IgnoreMe {
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ trait Route {
Response(r.future.flatMap{ r =>
if(r == null) {
val r2 = x.apply(request)
assert(r2 != null, "Route returned null: " + x + " for " + request)
r2.future
} else FXFuture.unit(r)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package one.jpro.platform.routing.performance

import com.jpro.webapi.WebAPI
import javafx.scene.Node
import one.jpro.platform.routing.SessionManagerContext
import one.jpro.platform.routing.sessionmanager.SessionManager
import simplefx.experimental._
import simplefx.core._
import simplefx.all._

object IncrementalLoading {

/**
* This should be called, before the node is added to the scene.
*/
def loadNode(node: Node): Node = {
if(WebAPI.isBrowser) {
node.setVisible(false)
getContext(node).map { ctx =>
ctx.enqueueNode(node)
}
}
node
}


private class IncrementalLoader(node: Node) {
var toMakeVisible: List[Node] = Nil

def enqueueNode(node: Node): Unit = {
toMakeVisible ::= node
if(toMakeVisible.size == 1) startIncrementalLoading()
}
def startIncrementalLoading(): Unit = {
// We are sure the node is in the scene
val webAPI = WebAPI.getWebAPI(node.scene)

def makeNextVisible(): Unit = {
webAPI.runAfterUpdate(new Runnable {
override def run(): Unit = {
if (!toMakeVisible.isEmpty) {
toMakeVisible.reverse.head.setVisible(true)
toMakeVisible = toMakeVisible.reverse.tail.reverse
nextFrame --> {
makeNextVisible()
}
}
}
})
}
runLater(makeNextVisible())
}
}

private object IncrementalLoadingKey extends AnyRef

private def getContext(x: Node): FXFuture[IncrementalLoader] = {
FXFuture.whenTrue(x.scene != null).map { _ =>
val webAPI = WebAPI.getWebAPI(x.scene)

val sm: SessionManager = SessionManagerContext.getContext(x)
if(sm == null) throw new RuntimeException("No JPro Rotuing SessionManager found")
val view = sm.view
val content = view.realContent
if(!content.getProperties.containsKey(IncrementalLoadingKey)) {
val loader = new IncrementalLoader(content)
content.getProperties.put(IncrementalLoadingKey, loader)
}
content.getProperties.get(IncrementalLoadingKey).asInstanceOf[IncrementalLoader]

}
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
.devfilter-hbox,

.devfilter-vbox {
-fx-background-color: #f0f0f0;
}

.devfilter-hbox {
-fx-alignment: center;
-fx-padding: 6 0 6 0;
-fx-padding: 6;
-fx-spacing: 16;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
.statisticsfilter-vbox {
-fx-background-color: #f0f0f0;
}
.statisticsfilter-hbox {
-fx-alignment: center;
-fx-padding: 6;
-fx-spacing: 16;
-fx-spacing: 8;
}

.statisticsfilter-statbox {
Expand All @@ -20,14 +23,15 @@
}

.statisticsfilter-statbox-values {
-fx-padding: 0 8 0 8;
-fx-padding: 0 4 0 4;
-fx-alignment: center-right;
}

.statisticsfilter-statbox-values .label {
-fx-font-size: 12px;
-fx-font-weight: bold;
-fx-text-alignment: right;
-fx-min-width: 18px;
}

.statisticsfilter-statbox-units {
Expand All @@ -38,4 +42,5 @@
-fx-font-size: 12px;
-fx-font-weight: bold;
-fx-text-alignment: right;
-fx-min-width: 36px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,13 @@ object StatisticsFilter {
// text <-- ("Latency: " + webAPIData.latency + " ms")
//}

this <++ new StatBox() {
this <++ new StatBox(true) {
labels <++ new Label("Latency: ")
values <++ new Label() {
text <-- ("" + webAPIData.latency)
text <-- ("" + toTimeString(webAPIData.latency * (millisecond))._1)
}
units <++ new Label() {
text <-- ("" + toTimeString(webAPIData.latency * (millisecond))._2)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import one.jpro.platform.routing.Route._
import one.jpro.platform.routing.sessionmanager.SessionManager
import one.jpro.platform.routing._
import one.jpro.platform.routing.dev.{DevFilter, StatisticsFilter}
import one.jpro.platform.routing.performance.IncrementalLoading
import org.controlsfx.control.PopOver
import simplefx.all._
import simplefx.core._
Expand All @@ -30,6 +31,7 @@ class MyApp(stage: Stage) extends RouteNode(stage) {
.and(get("/pdf", (r) => Response.view(new PDFTest)))
.and(get("/leak", (r) => Response.view(new LeakingPage)))
.and(get("/collect", (r) => Response.view(new CollectingPage)))
.and(get("/incremental", (r) => Response.view(new IncrementalPage)))
.and(get("/jmemorybuddy", (r) => Response.view(new JMemoryBuddyPage)))
.and(get("/100", (r) => Response.view(new ManyNodes(100))))
.and(get("/200", (r) => Response.view(new ManyNodes(200))))
Expand Down Expand Up @@ -75,6 +77,7 @@ class Header(view: View, sessionManager: SessionManager) extends HBox {
this <++ new HeaderLink("3200" , "/3200" )
this <++ new HeaderLink("6400" , "/6400" )
this <++ new HeaderLink("leak" , "/leak" )
this <++ new HeaderLink("incremental" , "/incremental" )
this <++ new HeaderLink("collect" , "/collect" )
this <++ new HeaderLink("jmemorybuddy" , "/jmemorybuddy" )
this <++ new HeaderLink("No Link" , "" ) {
Expand Down Expand Up @@ -254,6 +257,19 @@ class SubView extends Page {
}
}

class IncrementalPage extends Page {
def title = "Incremental"
def description = "desc Incremental"

val content = new VBox {
(1 to 100).map { i =>
val n = new Label("Node " + i)
IncrementalLoading.loadNode(n)
this <++ n
}
}
}

class PDFTest extends Page {
def title = "pdf"
def description = "pdf desc"
Expand Down