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

Implement Client/Server Editing #1043

Merged
merged 17 commits into from
May 21, 2020
Merged

Implement Client/Server Editing #1043

merged 17 commits into from
May 21, 2020

Conversation

sc1f
Copy link
Contributor

@sc1f sc1f commented May 12, 2020

One of Perspective's main selling points is interoperability between perspective-viewer in the browser and perspective-python on the server. This PR furthers inter-op by enabling editing between a client and a server without the latency overhead of using a remote Table, i.e. proxying all operations to the server. By creating ports on the client and server, clients are able to make edits to a Perspective Table in browser memory, and automatically transfer their edits to the server and all other connected clients. Similarly, the server is able to propagate live updates to each of their connected clients, and state will remain automatically synchronized.

For a concise example, see the Python Examples folder, where the client_server_edits Python and HTML files show the simplest possible implementation of client/server editing. The benefits of using client/server editing as opposed to traditional remote table editing are numerous, including:

  • Lightning-fast performance on the client side, as the render is no longer tied to round-trips from the server.
  • All clients receive updates from the server as well as edits from all other clients.
  • Because the table the client sees is constructed within the client, it can be a subset/slice of the main dataset on the server.

The main tradeoff is loading time - remote tables load incredibly quickly, as there is no data to transport across the network besides what is being requested by the viewer. For a table to be constructed in the client-side, however, the client needs access to the entire table's dataset in Arrow form. The example provides a simple implementation of this, and for most datasets the tradeoff in loading time is far outweighed by increased performance.

Changelog

  • Add the make_port API to Table, and the getEditPort method to perspective-viewer, allowing clients and servers to create ports and enable client/server editing.
  • Changes the on_update API - depending on the language, parameters to on_update callback functions need to be changed in order for proper operation. issues with callback parameters are easily debuggable, as they will raise exceptions in both Javascript and Python.
    • In Javascript, on_update callbacks now receive only one parameter: an object containing the keys port_id and delta, depending on if cell or row mode is selected:
view.on_update(updated => {
  console.log(updated.port_id, updated.delta instanceof ArrayBuffer);
}, {mode: "row"});
  • In Python, on_update callbacks either receive the port_id argument by default, or they receive both port_id and delta (if the row mode was selected):
def callback_with_delta(port_id, delta):
  print(port_id, delta)

def callback_without_delta(port_id):
  print(port_id)

view.on_update(callback_with_delta, mode="row")
view.on_update(callback_without_delta)
  • Change the way we process batched updates and notify users in on_update, which will always fire with a port_id.
  • Adds APIs on perspective-viewer so that each viewer automatically gets a port that is used for editing.
  • Added support for passing arrow binaries to and from PerspectiveWebsocket (in JS) and PerspectiveTornadohandler (in Python).
  • Fixes a regex mismatch that caused issues on Firefox but not Chrome.
  • Adds tests + examples in Javascript and Python

@sc1f sc1f requested review from timkpaine and texodus May 12, 2020 00:16
@sc1f sc1f self-assigned this May 12, 2020
@sc1f sc1f added C++ enhancement Feature requests or improvements internal Internal refactoring and code quality improvement JS Python labels May 12, 2020
Copy link
Member

@texodus texodus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really, really cool feature, with awesome test coverage also!

Apologies for the long delay in reviewing and merging this. In my defense - I think a PR having its own Changelog is a pretty apt jump-the-shark moment in terms of monolithic PRs, even if only as this PR's exceptionally detailed description nor mini-Changelog will not show up in the lerna-changelog master CHANGELOG.md for releases. Which means you won't be able to reap those sweet, sweet internet points.

Totally worth the wait, this one PR is basically an entire release's worth of features in itself!

@@ -597,10 +646,16 @@ t_gnode::get_table_sptr() {
void
t_gnode::promote_column(const std::string& name, t_dtype new_type) {
PSP_TRACE_SENTINEL();
PSP_VERBOSE_ASSERT(m_init, "touching uninited object");
PSP_VERBOSE_ASSERT(m_init, "Cannot `promote_column` on an uninited gnode.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better error message! I assume you changed these to trace the segfault? Reminds me that a future improvement may be to completely replace PSP_VERBOSE_ASSERT which check object initialization only and replace with proper valgrind support in the build scripts, stricter clang warnings and our existing excellent test coverage?

t_uindex
t_gnode::num_output_ports() const {
return m_oports.size();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am OK with removing these too if you're keen.

@@ -32,7 +32,7 @@ const UpperLowerCaseTokenType = createToken({
});

// Create tokens for column names and computed function names
const column_name_regex_pattern = /(["'])(?<column_name>.*?[^\\])\1/y;
const column_name_regex_pattern = /(["'])(.*?[^\\])\1/y;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chrome bug?


// Whether the user should be notified - False if process_table exited
// early, True otherwise.
return result.m_should_notify_userspace;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could maybe even remove the concept of no-ops and drop m_should_notify_userspace altogether; instead, we can just say if _process_table made it past the empty-table-checks earlier in this method, we call notify_userspace. This may simplify some of the aggregate logic in the context_* classes as well since they won't need to report this information.

@@ -34,7 +34,7 @@ const JPMC_VERSIONS = [

const FINOS_VERSIONS = ["0.3.1", "0.3.0", "0.3.0-rc.3", "0.3.0-rc.2", "0.3.0-rc.1"];

const UMD_VERSIONS = ["0.4.5", "0.4.4", "0.4.2", "0.4.1", "0.4.0", "0.3.9", "0.3.8", "0.3.7", "0.3.6"];
const UMD_VERSIONS = ["0.4.8", "0.4.7", "0.4.6", "0.4.5", "0.4.4", "0.4.2", "0.4.1", "0.4.0", "0.3.9", "0.3.8", "0.3.7", "0.3.6"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating this - reminds me, we could just use this and dump the pre-finos versions I think now?

$ npm view @finos/perspective  versions  --json
[
  "0.3.0-rc.1",
  "0.3.0-rc.2",
  "0.3.0-rc.3",
  "0.3.0-rc.4",
  "0.3.0",
  "0.3.1",
  "0.3.4",
  "0.3.5",
  "0.3.6",
  "0.3.7",
  "0.3.8",
  "0.3.9",
  "0.4.0-rc.1",
  "0.4.0-rc.2",
  "0.4.0-rc.3",
  "0.4.0-rc.4",
  "0.4.0-rc.5",
  "0.4.0-rc.6",
  "0.4.0",
  "0.4.1",
  "0.4.2",
  "0.4.4",
  "0.4.5",
  "0.4.6",
  "0.4.7",
  "0.4.8"
]

* @async
*/
async getEditPort() {
return this._edit_port_lock;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be async - _edit_port_lock is a Promise.


// Resolve the edit port lock, which allows for `get_edit_port` to be
// called in arbitary order without ever returning a null value.
this._edit_port_lock.resolve(this._edit_port);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is unnecessary if we remove the await on line 272 above? I would also like to understand how this can lead to null for port, since it seems (on inspection) that the Promise does not defer the actual call to make_port().

I tried removing this on my branch and did not see errors in the tests, but the message implies this error condition is race-y ..

@@ -153,7 +153,8 @@ export class Server {
// post transferable data for arrow
if (msg.args && msg.args[0]) {
if (msg.method === "on_update" && msg.args[0]["mode"] === "row") {
this.post(result, [ev]);
// actual arrow is in the `delta`
this.post(result, [ev.delta]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most Understated Addition in this patch!

@texodus
Copy link
Member

texodus commented May 21, 2020

Thanks for the PR!

@texodus texodus merged commit b2914ed into master May 21, 2020
@texodus texodus deleted the ports branch May 21, 2020 03:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C++ enhancement Feature requests or improvements internal Internal refactoring and code quality improvement JS Python
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants