0

I'm trying to take output from a single process (P1) and perform parallel tasks on it using other processes (P2 and P3). So far so simple.

To do this I'm connecting P2 and P3 to the single out-port of P1. In my mind, this should mean that P1 emits packets through its out port that are picked up by both P2 and P3 simultaneously, in parallel.

What I'm finding is that P2 and P3 aren't started in parallel and instead one of the processes will wait until the other has finished processing (or at least it seems that way to me).

For example, here is a simple graph that should take a JSON input then simultaneously grab a timestamp and parse the JSON. Another timestamp is taken after parsing the JSON and this is used as a basic method for calculating how long the JSON parsing took.

enter image description here

Notice the ordering of the connections going from the ajax/Get out port (the timestamp connection was added last).

In this case the difference in the timestamps is around 5ms, which roughly lines up with how long the JSON parse takes in a non-NoFlo environment (it's actually a little longer in NoFlo for some reason).

Now take the same graph but this time the connection-order from the ajax/Get out port has changed (the parse connection was added last):

enter image description here

This time the difference between the timestamps is around 40–50ms, which is clearly a massive difference and far larger than what the parse takes outside of NoFlo.

I'd really appreciate it if someone can shed some light on the following:

  • Why are the timings so different depending on the connection order?
  • How can I ensure that the 2 connections coming from ajax/Get are triggered and run in parallel (ie. they don't wait on each other)?

If it helps, here's a JSON export of the graph from FlowHub.

I've also put together a simple graph using the CLI and have managed to get a better insight into the flow of the graph and perhaps shed some light on what might be causing this:

# This executes in the correct order, though likely by
# coincidence and not due to true parallelisation.
#
# Time1 is run and outputted before Time2.
#
Read(filesystem/ReadFile) OUT -> IN Time1(objects/GetCurrentTimestamp)
Read OUT -> IN Parse(strings/ParseJson)

# This executes the entire Parse path before going back to grab
# and output Time1.
#
# Time1 is run and outputted *after* Time2
# Read doesn't send a disconnect message to Parse until *after*
# Time 1 is outputted.
#
# Read doesn't send a disconnect message to Time1 until *after*
# the Parse path has finished disconnecting.
#
# Read(filesystem/ReadFile) OUT -> IN Parse(strings/ParseJson)
# Read OUT -> IN Time1(objects/GetCurrentTimestamp)

Time1 OUT -> IN Display1(core/Output)

Parse OUT -> IN Time2(objects/GetCurrentTimestamp)
Time2 OUT -> IN Display2(core/Output)

'sample.geojson' -> IN Read

When run with the Read to Time1 connection defined before Read to Parse then the network is in order, though I've noticed that Read waits until everything else has completed before firing a disconnect (is that right?):

DATA -> ENCODING Read() CONN
DATA -> ENCODING Read() DATA
DATA -> ENCODING Read() DISC
DATA -> IN Read() CONN
DATA -> IN Read() DATA
DATA -> IN Read() DISC
Read() OUT -> IN Time1() CONN
Read() OUT -> IN Time1() < sample.geojson
Read() OUT -> IN Parse() CONN
Read() OUT -> IN Parse() < sample.geojson
Parse() OUT -> IN Time2() CONN
Parse() OUT -> IN Time2() < sample.geojson
Read() OUT -> IN Time1() DATA
Time1() OUT -> IN Display1() CONN
Time1() OUT -> IN Display1() DATA
1422549101639
Read() OUT -> IN Parse() DATA
Parse() OUT -> IN Time2() DATA
Time2() OUT -> IN Display2() CONN
Time2() OUT -> IN Display2() DATA
1422549101647
Read() OUT -> IN Time1() > sample.geojson
Read() OUT -> IN Parse() > sample.geojson
Parse() OUT -> IN Time2() > sample.geojson
Read() OUT -> IN Time1() DISC
Time1() OUT -> IN Display1() DISC
Read() OUT -> IN Parse() DISC
Parse() OUT -> IN Time2() DISC
Time2() OUT -> IN Display2() DISC

If I switch the order so the Read to Parse connection is defined first then everything goes wrong and Time1 isn't even sent a packet from Read until the entire Parse path has completed (so Time1 is actually after Time2 now):

DATA -> ENCODING Read() CONN
DATA -> ENCODING Read() DATA
DATA -> ENCODING Read() DISC
DATA -> IN Read() CONN
DATA -> IN Read() DATA
DATA -> IN Read() DISC
Read() OUT -> IN Parse() CONN
Read() OUT -> IN Parse() < sample.geojson
Parse() OUT -> IN Time2() CONN
Parse() OUT -> IN Time2() < sample.geojson
Read() OUT -> IN Time1() CONN
Read() OUT -> IN Time1() < sample.geojson
Read() OUT -> IN Parse() DATA
Parse() OUT -> IN Time2() DATA
Time2() OUT -> IN Display2() CONN
Time2() OUT -> IN Display2() DATA
1422549406952
Read() OUT -> IN Time1() DATA
Time1() OUT -> IN Display1() CONN
Time1() OUT -> IN Display1() DATA
1422549406954
Read() OUT -> IN Parse() > sample.geojson
Parse() OUT -> IN Time2() > sample.geojson
Read() OUT -> IN Time1() > sample.geojson
Read() OUT -> IN Parse() DISC
Parse() OUT -> IN Time2() DISC
Time2() OUT -> IN Display2() DISC
Read() OUT -> IN Time1() DISC
Time1() OUT -> IN Display1() DISC

If this is correct behaviour, then how do I run the 2 branches in parallel without one blocking the other?

I've tried making every component asynchronous, I've tried both that and using the WirePattern, I've tried creating multiple out ports and sending the data through all of them at once. No joy – it always comes down to the order in which the first edges are connected. I'm pulling my hair out with this as it's completely blocking my use of NoFlo for ViziCities :(

Robin Hawkes
  • 688
  • 8
  • 24

2 Answers2

0

NoFlo can't do multiple things in parallel due to the single-threaded nature of the JavaScript engine. I/O calls run in their own threads, but their callbacks always return us to the main thread where NoFlo runs.

In NoFlo, as long as we're dealing with synchronous components (like everything else than ajax/Get in your example graph appears to be), we execute depth-first to ensure fast throughput.

This means that the sub-flow from the first outbound connection of ajax/Get runs to completion first, then the second.

What you would want to happen here is breadth-first execution instead of depth-first. There has been some discussion on enabling that via edge metadata, but until then a way to do this would be to add core/RepeatAsync nodes between the connections of interest.

In long term the other interesting approach would be to enable running parts of the flow in their own threads via RemoteSubgraph and Web Workers. Theoretically we could even run each process in its own thread, giving full compatibility with classical FBP. But this would come at a hefty start-up cost.

bergie
  • 930
  • 7
  • 8
  • Thanks for the detailed clarification about how NoFlo is processing things, that certainly helps clear some of this up. My thinking was that if I used all async components then that would at least allow the graph to run the chains in parallel, or some weird first-come first-serve disjointed version. I just need a graph that I can ensure doesn't wait for the entire chain before moving on. I did try using `core/RepeatAsync` and that didn't change the behaviour for the better, in fact it slowed things down! I tried it in all permutations between the components. Is this a bug? – Robin Hawkes Jan 30 '15 at 08:09
  • As an aside, Web Workers is something I've been thinking about as I already use it for the current processing system in ViziCities. A minor start-up cost would be fine (I don't experience much of one with ViziCities anyway) as the benefit of truly parallel processing of the graph would far outweigh that initial cost. – Robin Hawkes Jan 30 '15 at 08:19
0

I won't consider the browser example, it is indeed depth-first because of how browser-side JavaScript works, as bergie explained.

The CLI example is more interesting though, because noflo-nodejs uses EventEmitters extensively. It still isn't truly parallel but it is more concurrent.

What we see here is a side effect of the following:

  1. Events are processed in the order of their occurrence.
  2. The order in which branches are defined in the graph affects the order in which events occur.
  3. Most components are triggered by data event rather than disconnect. They don't wait for disconnect to process data and send the result along.

In the aggregate it explains why the first branch executes before the second branch and why disconnects follow after all data has been processed already.

It might give you an impression of pure synchrony here, but despite the facts listed above, the system is still concurrent. If Read sent multiple packets at a decent speed, you would see events for branch 1 and branch 2 intermixed.

Update:

Here's a common trick for turning a sync component into async:

setTimeout(function() {
  doTheSyncJob(); // actual code here
  callback();
}, 0);