Skip to content

Commit

Permalink
fix: prevent concurrent update
Browse files Browse the repository at this point in the history
  • Loading branch information
jourdain committed Feb 28, 2024
1 parent 1efa6b4 commit 67cd840
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 87 deletions.
21 changes: 19 additions & 2 deletions examples/vtk/cone.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from trame.app import get_server
from trame.ui.html import DivLayout
from trame.widgets import html
from trame.widgets import html, client
from trame_vtklocal.widgets import vtklocal
from trame.decorators import TrameApp, change

Expand Down Expand Up @@ -58,6 +58,7 @@ class DemoApp:
def __init__(self, server=None):
self.server = get_server(server, client_type=CLIENT_TYPE)
self.render_window, self.cone = create_vtk_pipeline()
self.server.state.update(dict(mem_blob=0, mem_vtk=0))
self.html_view = None
self.ui = self._ui()
print(self.ui)
Expand All @@ -71,8 +72,16 @@ def on_resolution_change(self, resolution, **kwargs):

def _ui(self):
with DivLayout(self.server) as layout:
client.Style("body { margin: 0; }")
self.html_view = vtklocal.LocalView(
self.render_window, style="width: 100vw; height: 100vh;"
self.render_window,
style="width: 100vw; height: 100vh;",
cache_size=("cache", 0),
memory="mem_blob = $event.blobs; mem_vtk = $event.scene;",
)
html.Div(
"Scene: {{ (mem_vtk / 1024).toFixed(1) }}KB - Arrays: {{ (mem_blob / 1024).toFixed(1) }}KB - cache: {{ (cache/1024).toFixed(1) }}KB ",
style="position: absolute; top: 1rem; left: 1rem; z-index: 10; background: white; padding: 1rem; border-radius: 1rem;",
)
html.Input(
type="range",
Expand All @@ -82,6 +91,14 @@ def _ui(self):
step=1,
style="position: absolute; top: 1rem; right: 1rem; z-index: 10;",
)
html.Input(
type="range",
v_model=("cache", 0),
min=0,
max=100000,
step=1000,
style="position: absolute; top: 1rem; right: 10rem; z-index: 10;",
)

return layout

Expand Down
44 changes: 32 additions & 12 deletions examples/vtk/glyph.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from trame.widgets import vuetify3 as vuetify
from trame.widgets.vtk import VtkRemoteView
from trame_vtklocal.widgets import vtklocal
from trame.decorators import TrameApp, change

from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkFiltersCore import vtkElevationFilter, vtkGlyph3D
from vtkmodules.vtkFiltersSources import vtkConeSource, vtkCubeSource, vtkSphereSource
from vtkmodules.vtkImagingCore import vtkRTAnalyticSource
from vtkmodules.vtkImagingGeneral import vtkImageGradient
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkPolyDataMapper,
Expand Down Expand Up @@ -98,38 +98,56 @@ def setup_vtk():

renWin = vtkRenderWindow()
renWin.AddRenderer(ren)
renWin.SetWindowName("GlyphTable")

iren = vtkRenderWindowInteractor()
istyle = vtkInteractorStyleTrackballCamera()
iren.SetInteractorStyle(istyle)
iren.SetRenderWindow(renWin)
renderWindowInteractor = vtkRenderWindowInteractor()
renderWindowInteractor.SetRenderWindow(renWin)
renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()

ren.ResetCamera()
return renWin, cs2, ss

return renWin, ren, cs2, ss


# -----------------------------------------------------------------------------
# GUI
# -----------------------------------------------------------------------------


@TrameApp()
class App:
def __init__(self, server=None):
self.server = get_server(server, client_type="vue3")
self.render_window, self.cone, self.sphere = setup_vtk()
self.render_window, self.renderer, self.cone, self.sphere = setup_vtk()
self.view_local = None
self.view_remote = None
self.ui = self._build_ui()

@property
def ctrl(self):
return self.server.controller

@change("resolution")
def on_resolution_change(self, resolution, **kwargs):
self.cone.SetResolution(int(resolution))
self.sphere.SetStartTheta(int(resolution) * 6)
self.view_local.update()
self.view_remote.update()

def reset_camera(self):
self.renderer.ResetCamera()
self.view_local.update()
self.view_remote.update()

def _build_ui(self):
with SinglePageLayout(self.server) as layout:
layout.icon.click = self.ctrl.view_reset_camera
layout.icon.click = self.reset_camera
with layout.toolbar:
vuetify.VSpacer()
vuetify.VSlider(
v_model=("resolution", 6),
min=3,
max=60,
step=1,
dense=True,
hide_details=True,
)
Expand All @@ -143,12 +161,14 @@ def _build_ui(self):
with vuetify.VContainer(
fluid=True, classes="pa-0 fill-height", style="width: 50%;"
):
view = vtklocal.LocalView(self.render_window)
self.ctrl.view_update = view.update
self.view_local = vtklocal.LocalView(self.render_window)
self.ctrl.view_update = self.view_local.update
with vuetify.VContainer(
fluid=True, classes="pa-0 fill-height", style="width: 50%;"
):
VtkRemoteView(self.render_window)
self.view_remote = VtkRemoteView(
self.render_window, interactive_ratio=1
)

# hide footer
layout.footer.hide()
Expand Down
6 changes: 4 additions & 2 deletions trame_vtklocal/widgets/vtklocal.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ def __init__(self, render_window, **kwargs):

self._attributes["rw_id"] = f'render-window="{self._window_id}"'
self._attributes["ref"] = f'ref="{self.__ref}"'
self._attr_names += []
self._event_names += []
self._attr_names += [
("cache_size", "cacheSize"),
]
self._event_names += ["updated", "memory"]

@property
def api(self):
Expand Down
164 changes: 93 additions & 71 deletions vue-components/src/components/VtkLocal.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ export default {
const hashesMTime = {};
const currentMTime = ref(1);
let objectManager = null;
let updateInProgress = 0;

function resize() {
const { width, height } = container.value.getBoundingClientRect();
const w = Math.floor(width * window.devicePixelRatio + 0.5);
const h = Math.floor(height * window.devicePixelRatio + 0.5);
canvasWidth.value = w;
canvasHeight.value = h;
console.log(`vtkLocal::resize ${width}x${height}`);
// console.log(`vtkLocal::resize ${width}x${height}`);
if (props.renderWindow.length > 0) {
nextTick(() => {
objectManager.setSize(props.renderWindow, w, h);
Expand All @@ -48,102 +49,123 @@ export default {
const serverState = await session.call("vtklocal.get.state", [vtkId]);
if (serverState.length > 0) {
stateMTimes[vtkId] = JSON.parse(serverState).MTime;
console.log(`vtkLocal::state(${vtkId})`);
// console.log(`vtkLocal::state(${vtkId})`);
objectManager.registerState(serverState);
} else {
throw new Error(`Server returned empty state for ${vtkId}`);
console.log(`Server returned empty state for ${vtkId}`);
// throw new Error(`Server returned empty state for ${vtkId}`);
}
return serverState;
}
async function fetchHash(hash) {
const session = client.getConnection().getSession();
// console.log(`vtkLocal::hash(${hash}) - before`);
const blob = await session.call("vtklocal.get.hash", [hash]);
console.log(`vtkLocal::hash(${hash})`);
const array = new Uint8Array(await blob.arrayBuffer());
objectManager.registerBlob(hash, array);
// console.log(`vtkLocal::hash(${hash}) - available`);
hashesMTime[hash] = unref(currentMTime);
return blob;
}

async function update(startEventLoop = false) {
console.log("vtkLocal::update(begin)");
const session = client.getConnection().getSession();
const serverStatus = await session.call("vtklocal.get.status", [
props.renderWindow,
]);
const pendingRequests = [];
console.log("ids", serverStatus.ids);
serverStatus.ids.forEach(([vtkId, mtime]) => {
if (!stateMTimes[vtkId] || stateMTimes[vtkId] < mtime) {
console.log("fetch", vtkId);
pendingRequests.push(fetchState(vtkId));
} else {
console.log("skip", vtkId);
}
});
serverStatus.ignore_ids.forEach((vtkId) => {
objectManager.unRegisterState(vtkId);
});
serverStatus.hashes.forEach((hash) => {
if (!hashesMTime[hash]) {
pendingRequests.push(fetchHash(hash));
}
hashesMTime[hash] = unref(currentMTime);
});
await Promise.all(pendingRequests);
// Shows memory, feel free to remove.
console.log(
`vtkLocal::update(end) blobs: ${
objectManager.getTotalBlobMemoryUsage() / 1024
}kB, objects: ${
objectManager.getTotalVTKDataObjectMemoryUsage() / 1024
}kB`
);
objectManager.update(startEventLoop);
resize();
emit("updated");
updateInProgress++;
if (updateInProgress !== 1) {
// console.error("Skip concurrent update");
return;
}

// Memory management
currentMTime.value++;
const threshold =
props.cacheSize + objectManager.getTotalVTKDataObjectMemoryUsage();
if (objectManager.getTotalBlobMemoryUsage() > threshold) {
// Need to remove old blobs
const threshold =
props.cacheSize + objectManager.getTotalVTKDataObjectMemoryUsage();
const tsMap = {};
let mtimeToFree = unref(currentMTime);
Object.entries(hashesMTime).forEach(([hash, mtime]) => {
if (mtime < mtimeToFree) {
mtimeToFree = mtime;
}
const sMtime = mtime.toString();
if (tsMap[sMtime]) {
tsMap[sMtime].push(hash);
try {
// console.log("vtkLocal::update(begin)");
const session = client.getConnection().getSession();
const serverStatus = await session.call("vtklocal.get.status", [
props.renderWindow,
]);
const pendingRequests = [];
// console.log("ids", serverStatus.ids);
serverStatus.ids.forEach(([vtkId, mtime]) => {
if (!stateMTimes[vtkId] || stateMTimes[vtkId] < mtime) {
// console.log("fetch", vtkId);
pendingRequests.push(fetchState(vtkId));
} else {
tsMap[sMtime] = [hash];
// console.log("skip", vtkId);
}
});
serverStatus.ignore_ids.forEach((vtkId) => {
objectManager.unRegisterState(vtkId);
});
serverStatus.hashes.forEach((hash) => {
if (!hashesMTime[hash]) {
pendingRequests.push(fetchHash(hash));
}
hashesMTime[hash] = unref(currentMTime);
});
await Promise.all(pendingRequests);
try {
objectManager.update(startEventLoop);
} catch (e) {
console.error("WASM update failed");
console.log(e);
}
resize();
emit("updated");

// Remove blobs starting by the old ones
while (objectManager.getTotalBlobMemoryUsage() > threshold) {
const hashesToDelete = tsMap[mtimeToFree];
for (let i = 0; i < hashesToDelete.length; i++) {
objectManager.unRegisterBlob(hashesToDelete[i]);
// Memory management
currentMTime.value++;
const threshold =
Number(props.cacheSize) +
objectManager.getTotalVTKDataObjectMemoryUsage();
if (objectManager.getTotalBlobMemoryUsage() > threshold) {
// console.log("Free memory");
// Need to remove old blobs
const tsMap = {};
let mtimeToFree = unref(currentMTime);
Object.entries(hashesMTime).forEach(([hash, mtime]) => {
if (mtime < mtimeToFree) {
mtimeToFree = mtime;
}
const sMtime = mtime.toString();
if (tsMap[sMtime]) {
tsMap[sMtime].push(hash);
} else {
tsMap[sMtime] = [hash];
}
});

// Remove blobs starting by the old ones
while (objectManager.getTotalBlobMemoryUsage() > threshold) {
const hashesToDelete = tsMap[mtimeToFree];
if (hashesToDelete) {
for (let i = 0; i < hashesToDelete.length; i++) {
// console.log(
// `Delete hash(${hashesToDelete[i]}) - mtime: ${mtimeToFree}`
// );
objectManager.unRegisterBlob(hashesToDelete[i]);
delete hashesMTime[hashesToDelete[i]];
}
}
mtimeToFree++;
}
mtimeToFree++;
}
emit("memory", {
blobs: objectManager.getTotalBlobMemoryUsage(),
scene: objectManager.getTotalVTKDataObjectMemoryUsage(),
});
} catch (e) {
console.error("Error in update", e);
} finally {
updateInProgress--;
if (updateInProgress) {
updateInProgress = 0;
update();
}
}
emit("memory", {
blobs: objectManager.getTotalBlobMemoryUsage(),
scene: objectManager.getTotalVTKDataObjectMemoryUsage(),
});
}

onMounted(async () => {
console.log("vtkLocal::mounted");
// console.log("vtkLocal::mounted");
objectManager = await createModule(unref(canvas));
console.log("objectManager", objectManager);
// console.log("objectManager", objectManager);
resizeObserver.observe(unref(container));
update(/*startEventLoop=*/ true);
setTimeout(() => {
Expand All @@ -154,7 +176,7 @@ export default {
});

onBeforeUnmount(() => {
console.log("vtkLocal::unmounted");
// console.log("vtkLocal::unmounted");
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
Expand Down

0 comments on commit 67cd840

Please sign in to comment.