Skip to content

Commit 4da9243

Browse files
authored
Merge pull request #3013 from finos/fix-circular
Fix examples, docs and imports
2 parents da1e971 + ba905d9 commit 4da9243

28 files changed

Lines changed: 98 additions & 1549 deletions

File tree

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
11
# Multi-threading
22

3-
Perspective's API is thread-safe (safe to call from multiple threads
4-
concurrently),
3+
Perspective's API is thread-safe, so methods may be called from different
4+
threads without additional consideration for safety/exclusivity/correctness. All
5+
`perspective.Client` and `perspective.Server` API methods release the GIL, which
6+
can be exploited for parallelism.
57

6-
Perspective's server API releases the GIL when called (though it may be retained
7-
for some portion of the `Client` call to encode RPC messages). It also
8-
dispatches to an internal thread pool for some operations, enabling better
9-
parallelism and overall better server performance. However, Perspective's Python
10-
interface itself will still process queries in a single queue. To enable
11-
parallel query processing, call `set_loop_callback` with a multi-threaded
12-
executor such as `concurrent.futures.ThreadPoolExecutor`:
8+
Interally, `perspective.Server` also dispatches to a thread pool for some
9+
operations, enabling better parallelism and overall better query performance.
10+
This independent threadpool size can be controlled via
11+
`perspective.set_num_cpus()`, or the `OMP_NUM_THREADS` environment variable.
1312

1413
```python
15-
def perspective_thread():
16-
server = perspective.Server()
17-
loop = tornado.ioloop.IOLoop()
18-
with concurrent.futures.ThreadPoolExecutor() as executor:
19-
server.set_loop_callback(loop.run_in_executor, executor)
20-
loop.start()
14+
import perspective
15+
16+
perspective.set_num_cpus(2)
17+
```
18+
19+
## Server handlers
20+
21+
Perspective's server handler implementations each take an optional `executor`
22+
constructor argument, which (when provided) will configure the handler to
23+
process WebSocket `Client` requests on a thread pool.
24+
25+
```python
26+
from concurrent.futures import ThreadPoolExecutor
27+
from tornado.web import Application
28+
from perspective.handlers.tornado import PerspectiveTornadoHandler
29+
from perspective import Server
30+
31+
args = {"perspective_server": Server(), "executor": ThreadPoolExecutor()}
32+
33+
app = Application(
34+
[
35+
(r"/websocket", PerspectiveTornadoHandler, args),
36+
37+
# ...
38+
39+
]
40+
)
2141
```

examples/esbuild-remote/build.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ async function build() {
2424
loader: {
2525
".ttf": "file",
2626
".arrow": "file",
27+
".wasm": "file",
2728
},
2829
assetNames: "[name]",
2930
});

examples/esbuild-remote/client/index.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,22 @@
1212

1313
import perspective from "@finos/perspective";
1414

15-
import "@finos/perspective-viewer";
15+
import perspective_viewer from "@finos/perspective-viewer";
1616
import "@finos/perspective-viewer-datagrid";
1717
import "@finos/perspective-viewer-d3fc";
1818

1919
import "@finos/perspective-viewer/dist/css/pro-dark.css";
2020

2121
import "./index.css";
2222

23+
import SERVER_WASM from "@finos/perspective/dist/wasm/perspective-server.wasm";
24+
import CLIENT_WASM from "@finos/perspective-viewer/dist/wasm/perspective-viewer.wasm";
25+
26+
await Promise.all([
27+
perspective.init_server(fetch(SERVER_WASM)),
28+
perspective_viewer.init_client(fetch(CLIENT_WASM)),
29+
]);
30+
2331
const viewer = document.createElement("perspective-viewer");
2432
document.body.append(viewer);
2533

examples/python-aiohttp/server.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@
1010
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13-
import asyncio
1413
import os
1514
import os.path
1615
import logging
17-
import threading
1816

1917
from aiohttp import web
2018

@@ -32,22 +30,13 @@ def perspective_thread(server):
3230
"""Perspective application thread starts its own event loop, and
3331
adds the table with the name "data_source_one", which will be used
3432
in the front-end."""
35-
psp_loop = asyncio.new_event_loop()
36-
client = server.new_local_client(loop_callback=psp_loop.call_soon_threadsafe)
37-
38-
def init():
39-
with open(file_path, mode="rb") as file:
40-
client.table(file.read(), index="Row ID", name="data_source_one")
41-
42-
psp_loop.call_soon_threadsafe(init)
43-
psp_loop.run_forever()
4433

4534

4635
def make_app():
4736
server = Server()
48-
thread = threading.Thread(target=perspective_thread, args=(server,))
49-
thread.daemon = True
50-
thread.start()
37+
client = server.new_local_client()
38+
with open(file_path, mode="rb") as file:
39+
client.table(file.read(), index="Row ID", name="data_source_one")
5140

5241
async def websocket_handler(request):
5342
handler = PerspectiveAIOHTTPHandler(perspective_server=server, request=request)

examples/python-starlette/server.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@
1010
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13-
import asyncio
1413
import os
1514
import os.path
1615
import logging
17-
import threading
1816
import uvicorn
1917

2018
from fastapi import FastAPI, WebSocket
@@ -35,27 +33,11 @@ def static_node_modules_handler(rest_of_path):
3533
return FileResponse("../../node_modules/{}".format(rest_of_path))
3634

3735

38-
def perspective_thread(server):
39-
"""Perspective application thread starts its own event loop, and
40-
adds the table with the name "data_source_one", which will be used
41-
in the front-end."""
42-
psp_loop = asyncio.new_event_loop()
43-
client = server.new_local_client(loop_callback=psp_loop.call_soon_threadsafe)
44-
45-
def init():
46-
with open(file_path, mode="rb") as file:
47-
client.table(file.read(), index="Row ID", name="data_source_one")
48-
49-
psp_loop.call_soon_threadsafe(init)
50-
psp_loop.run_forever()
51-
52-
5336
def make_app():
5437
server = Server()
55-
56-
thread = threading.Thread(target=perspective_thread, args=(server,))
57-
thread.daemon = True
58-
thread.start()
38+
client = server.new_local_client()
39+
with open(file_path, mode="rb") as file:
40+
client.table(file.read(), index="Row ID", name="data_source_one")
5941

6042
async def websocket_handler(websocket: WebSocket):
6143
handler = PerspectiveStarletteHandler(

examples/python-tornado/server.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,6 @@
2626
)
2727

2828

29-
async def init_table(client):
30-
with open(file_path, mode="rb") as file:
31-
data = file.read()
32-
table = client.table(data, name="data_source_one")
33-
for _ in range(10):
34-
table.update(data)
35-
36-
3729
def make_app(perspective_server):
3830
return tornado.web.Application(
3931
[
@@ -62,6 +54,11 @@ def make_app(perspective_server):
6254
app.listen(8080)
6355
logging.critical("Listening on http://localhost:8080")
6456
loop = tornado.ioloop.IOLoop.current()
65-
client = perspective_server.new_local_client(loop_callback=loop.add_callback)
66-
loop.call_later(0, init_table, client)
57+
client = perspective_server.new_local_client()
58+
with open(file_path, mode="rb") as file:
59+
data = file.read()
60+
table = client.table(data, name="data_source_one")
61+
for _ in range(10):
62+
table.update(data)
63+
6764
loop.start()

packages/perspective-viewer-d3fc/src/ts/plugin/d3fc_global_styles.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,24 @@
1010
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

13-
export const D3FC_GLOBAL_STYLES = [];
14-
1513
// Capture (and restore) `document.querySelector` to prevent D3FC from
1614
// attaching styles to the document `<head>`.
1715
export async function init() {
1816
const old_doc = window.document.querySelector;
17+
const stylesheets = [];
18+
1919
// @ts-ignore
2020
window.document.querySelector = () => {
2121
return {
2222
appendChild(elem) {
23-
D3FC_GLOBAL_STYLES.push(elem);
23+
stylesheets.push(elem);
2424
return elem;
2525
},
2626
};
2727
};
2828

29-
const { register } = await import("./plugin");
29+
const { register, createStyleSheets } = await import("./plugin");
3030
window.document.querySelector = old_doc;
31+
createStyleSheets(stylesheets);
3132
register();
3233
}

packages/perspective-viewer-d3fc/src/ts/plugin/plugin.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { HTMLPerspectiveViewerElement } from "@finos/perspective-viewer";
1818
import type * as psp_types from "@finos/perspective-viewer";
1919

2020
import * as d3 from "d3";
21-
import { D3FC_GLOBAL_STYLES } from "./d3fc_global_styles";
21+
2222
import { Chart, Settings, Type } from "../types";
2323

2424
const DEFAULT_PLUGIN_SETTINGS = {
@@ -30,17 +30,7 @@ const DEFAULT_PLUGIN_SETTINGS = {
3030
selectMode: "select",
3131
};
3232

33-
const styleSheets = [];
34-
for (const style of D3FC_GLOBAL_STYLES) {
35-
const sheet = new CSSStyleSheet();
36-
sheet.replaceSync(style.textContent);
37-
styleSheets.push(sheet);
38-
}
39-
40-
const sheet = new CSSStyleSheet();
41-
sheet.replaceSync(style);
42-
43-
styleSheets.push(sheet);
33+
const D3FC_GLOBAL_STYLES = [];
4434

4535
const EXCLUDED_SETTINGS = [
4636
"crossValues",
@@ -63,6 +53,18 @@ async function register_element(plugin_name: string) {
6353
await perspectiveViewerClass.registerPlugin(plugin_name);
6454
}
6555

56+
export function createStyleSheets(sheets) {
57+
for (const style of sheets) {
58+
const sheet = new CSSStyleSheet();
59+
sheet.replaceSync(style.textContent);
60+
D3FC_GLOBAL_STYLES.push(sheet);
61+
}
62+
63+
const sheet = new CSSStyleSheet();
64+
sheet.replaceSync(style);
65+
D3FC_GLOBAL_STYLES.push(sheet);
66+
}
67+
6668
export function register(...plugin_names: string[]) {
6769
const plugins = new Set(
6870
plugin_names.length > 0
@@ -115,7 +117,7 @@ class HTMLPerspectiveViewerD3fcPluginElement extends HTMLElement {
115117
connectedCallback() {
116118
if (!this._initialized) {
117119
this.attachShadow({ mode: "open" });
118-
for (const sheet of styleSheets) {
120+
for (const sheet of D3FC_GLOBAL_STYLES) {
119121
this.shadowRoot.adoptedStyleSheets.push(sheet);
120122
}
121123

packages/perspective-viewer-datagrid/src/js/custom_elements/datagrid.js

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,36 @@ import { format_raw } from "../data_listener/format_cell.js";
2323
* The custom element class for this plugin. The interface methods for this
2424
*/
2525
export class HTMLPerspectiveViewerDatagridPluginElement extends HTMLElement {
26+
static global_stylesheet_installed = false;
27+
static renderTarget =
28+
window.CSS?.supports &&
29+
window.CSS?.supports("selector(:host-context(foo))")
30+
? "shadow"
31+
: "light";
32+
33+
static #sheet;
34+
2635
constructor() {
2736
super();
2837
this.regular_table = document.createElement("regular-table");
2938
this.regular_table.part = "regular-table";
3039
this._is_scroll_lock = false;
3140
this._edit_mode = "READ_ONLY";
3241
const Elem = HTMLPerspectiveViewerDatagridPluginElement;
33-
if (Elem.renderTarget == "shadow") {
34-
if (!Elem.#sheet) {
35-
Elem.#sheet = new CSSStyleSheet();
36-
Elem.#sheet.replaceSync(datagridStyles);
37-
}
42+
if (!Elem.#sheet) {
43+
Elem.#sheet = new CSSStyleSheet();
44+
Elem.#sheet.replaceSync(datagridStyles);
45+
}
3846

47+
if (Elem.renderTarget === "shadow") {
3948
const shadow = this.attachShadow({ mode: "open" });
4049
shadow.adoptedStyleSheets.push(Elem.#sheet);
50+
} else if (
51+
Elem.renderTarget === "light" &&
52+
!Elem.global_stylesheet_installed
53+
) {
54+
Elem.global_stylesheet_installed = true;
55+
document.adoptedStyleSheets.push(Elem.#sheet);
4156
}
4257
}
4358

@@ -177,11 +192,4 @@ export class HTMLPerspectiveViewerDatagridPluginElement extends HTMLElement {
177192
}
178193
this.regular_table.clear();
179194
}
180-
181-
static renderTarget =
182-
window.CSS?.supports &&
183-
window.CSS?.supports("selector(:host-context(foo))")
184-
? "shadow"
185-
: "light";
186-
static #sheet;
187195
}

rust/perspective-python/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ authors = ["Andrew Stein <steinlink@gmail.com>"]
2222
keywords = []
2323
build = "build.rs"
2424
include = [
25-
"bench/**/*.py",
2625
"build.rs",
2726
"./Cargo.toml",
2827
"LICENSE.md",

0 commit comments

Comments
 (0)