News for hackers. Stuff that matters.
Re:Node.js in Flame Graphs by thedufer on Wednesday November 19, 2014 @12:11PM (#8631196)
> It’s unclear why Express.js chose not to use a constant time data structure like a map to store its handlers.

Its actually quite clear - most routes are defined by a regex rather than a string, so there is no built-in structure (if there's a way at all) to do O(1) lookups in the routing table. A router that only allowed string route definitions would be faster but far less useful.

I can't explain away the recursion, though. That seems wholly unnecessary.

Edit: Actually, I figured that out, too. You can put middleware in a router so it only runs on certain URL patterns. The only difference between a normal route handler and a middleware function is that a middleware function uses the third argument (an optional callback) and calls it when done to allow the route matcher to continue through the routes array. This can be asynchronous (thus the callback), so the router has to recurse through the routes array instead of looping.

Re:Node.js in Flame Graphs by quotemstr on Wednesday November 19, 2014 @12:23PM (#8631268)
Of course there's a faster way! Combine all the routes into a DFA, then run the DFA over the URL. It's guaranteed to run in constant space and O(n) (n=URL length) time! The union of any set of regular languages is itself a regular language.

You can use Ragel[1] to build your automaton.

[1] http://www.colm.net/open-source/ragel/

Re:Node.js in Flame Graphs by badgersandjam on Wednesday November 19, 2014 @02:19PM (#8632023)
This. I wrote something that did this a few years ago. It took n patterns (not regex, simpler) and turned them into one DFA state table with a function ptr stored for each final state. Then it had a tiny runtime. Never thought of using it for route tables however.

Nice idea.

Edit: I did consider rewriting the runtime as a forth style interpreter as well but the time never appeared.

Re:Node.js in Flame Graphs by rtkwe on Wednesday November 19, 2014 @04:18PM (#8632680)
How long does it take to build the DFA when a new route is added? That seems like the major time performance problem point if new routes are added often or regularly.
Re:Node.js in Flame Graphs by badgersandjam on Wednesday November 19, 2014 @04:35PM (#8632748)
To be honest I didn't time it and it wasn't thread safe and I don't have the source any more so I can't answer that. It doesn't do a lot so I imagine it would be relatively cheap.

The function it performed to rebuild the DFA was to create an NFA for the new pattern stream (it worked on char *) and then walk both the existing master NFA and the merge them. Then it was converted from NFA to DFA via dragon book copy and paste algorithm (my discrete math isn't great). Both NFA and DFA were represented as a bunch of C structs tied together by pointers. Then a table generator walked the new DFA and generated a new state table. It was very naive but covered the common case where large parts of the byte stream to be matched were similar.

I've got MacVim up and writing it again. May post it as a Show HN if I get the time to finish it.

Re:Node.js in Flame Graphs by TheLoneWolfling on Thursday November 20, 2014 @10:49AM (#8636264)
Worst case is absurd, yes. But most (all?) added routes that exhibit that behavior are routes that would exhibit that behavior when run as a regex (and hence converted into a DFA) on their own. (I.e. generally if you would shoot yourself in the foot with this method you've already shot yourself in the foot.)

Your worst case is O(nd + O(constructing the new route's DFA)), where "n" is the number of nodes in the previous DFA and "d" is the number of nodes in the DFA of the new route.

Note that constructing r's DFA can be done beforehand, and hence the actual time the router will be offline is only O(nd).

(Assuming a simple 2 DFA -> 1 NFA -> DFA merge. Your worst case is that every possible pair from the two, and singleton of the two is present in the final DFA, i.e. ({node from old, node from new} -> nd + {node from old} -> n + {node from new} -> d) -> O(nd + n + d) -> O(n*d). Note that this isn't the classic O(2^n) behavior, because we know that, due to the old DFA and new route DFA being DFAs, only one node in n and one node in d can be present at a time in the final stateset.

A better bet may be to use the classic "lazily-constructed DFA" approach. In other words, keep it as an NFA while only converting (sets of) nodes to DFA nodes when necessary. If you are worried about memory, you can even do this only for "hot" sections of the NFA, or only for those sections of the NFA that have the most performance gain for the number of nodes added.

Re:Node.js in Flame Graphs by arcatek on Wednesday November 19, 2014 @08:12PM (#8633835)
I'm not sure I can think of a case where new routes should be added dynamically, is there ?
Re:Node.js in Flame Graphs by jonny_eh on Wednesday November 19, 2014 @08:37PM (#8633925)
That's the exact thing that caused Netflix's problem. Read the article.
Re:Node.js in Flame Graphs by rtpg on Wednesday November 19, 2014 @10:16PM (#8634256)
This is the reason we need more people in JS land taking compiler courses and the like. There's a lot of tech out there that are speed sensitive yet do not apply the "best" solution to a problem solved in the 70s.
Re:Node.js in Flame Graphs by yzzxy on Thursday November 20, 2014 @12:41AM (#8634569)
But then you need to seduce the JS runtime into actually performing your optimizations. Maybe time for C/C++ extensions to become more prevalent a la Python?
Re:Node.js in Flame Graphs by andrewstuart2 on Wednesday November 19, 2014 @05:11PM (#8632997)
This approach will of course work, but you can't have a middleware stack with a defined order using that approach, unless I'm mistaken.

Sure, you could use all routes that match, but is there a way to specify the order for all handlers, and whether or not you should continue after one handler is done?

Re:Node.js in Flame Graphs by nostrademons on Wednesday November 19, 2014 @06:19PM (#8633354)
The easiest way is to not conflate middleware handling with routing. Your routing infrastructure should take a dict mapping URL patterns to callables, and returns a callable (which uses this DFA-based approach to dispatch). Your middleware should be a callable that wraps another callable. If you want to apply middleware to just one path, wrap it before you register it with the routing table. If you want to apply middleware to all (or a subset of) paths, wrap the returned routing table, and optionally stick that in some other routing table.
Re:Node.js in Flame Graphs by quotemstr on Thursday November 20, 2014 @12:23PM (#8636843)
> You can't have a middleware stack with a defined order using that approach

Sure you can. When you build your NFA, each accepting state is tagged with the route to which it corresponds. Converting to a DFA will merge states. Any accepting state in the DFA tagged with more than one route indicates a possible ambiguity. How you handle that ambiguity is up to you.

One strategy is to just fail at route-building time. Another is to order the routes by priority (which priorities maybe coming from order in the source code) and eliminate from each accepting state all routes except the one with the highest priority.

The latter approach gives you the "defined order" semantics you want.

Re:Node.js in Flame Graphs by ChuckMcM on Wednesday November 19, 2014 @04:03PM (#8632605)
Oh that is very cool, I am definitely going to have to spend some time playing with that.
Re:Node.js in Flame Graphs by thedufer on Wednesday November 19, 2014 @12:30PM (#8631320)
Yeah, but what's n there? Isn't that going to be something like the sum of each route's length? That doesn't buy us anything (well, probably a smaller constant).

Unless you go the NFA route, but I'm pretty sure that costs non-constant space.

Re:Node.js in Flame Graphs by colanderman on Wednesday November 19, 2014 @12:32PM (#8631338)
No. n in the case of a DFA is the length of the input string. (i.e. the string being matched.)

So, in terms of the GP's post, yes, this is an O(1) (with respect to number of routes) solution.

Re:Node.js in Flame Graphs by thedufer on Wednesday November 19, 2014 @12:34PM (#8631359)
Oh, right. My CS has gotten fuzzy - the tree gets enormous, but the runtime stays is O(n) on input length. Thanks for the explanation.
Re:Node.js in Flame Graphs by quotemstr on Wednesday November 19, 2014 @01:00PM (#8631513)
The automaton isn't even that big, really. The number of NFA states is roughly proportional to the total number of characters in the regular expressions involved, and NFA to DFA conversion usually expands the automaton only be a factor of two or three. Although the subset construction in theory worst-case exponential, that situation never occurs in practice.
Re:Node.js in Flame Graphs by thedufer on Wednesday November 19, 2014 @01:33PM (#8631715)
By that logic the app I work on would have a DFA with ~200,000 nodes. That sounds fairly large to me.

I suspect this is a slight overestimate, but the point stands - non-trivial apps could quickly build very large state graphs.

Re:Node.js in Flame Graphs by jerf on Wednesday November 19, 2014 @01:54PM (#8631838)
Yes, but unless you're running on embedded, and to be honest to some extent even then unless you're way at the bottom of the power scale, you're running on a supercomputer that will laugh at 200,000 nodes in a DFA, and it's very easy for that DFA to run significantly faster than the presumably several tens of thousands of FAs you're running sequentially otherwise.
Re:Node.js in Flame Graphs by ufo on Wednesday November 19, 2014 @02:07PM (#8631944)
The exponential blowup only occurs in the worst case. I don't have a proof at hand but I suspect that the final automaton shouldn't grow very large if your regexes are modeling web routes, since they are structured as a tree of paths with lots of "prefix-sharing" and no loops.

Other than that, if you are concerned about automaton size you can use a non-deterministc automaton instead of a deterministic one. You spend a bit more time (at each iteration you have to update a list of "active" states, while in the DFA case there is always a single active state) but on the other hand the automaton itself is much more compact.

Re:Node.js in Flame Graphs by danieldk on Wednesday November 19, 2014 @01:55PM (#8631849)
By that logic the app I work on would have a DFA with ~200,000 nodes. That sounds fairly large to me.

That's not very large, since you can store a DFA very compactly in a table (C array with structs). The table stores the transitions, which only have the character and the target state. Then you either keep an array with state offsets in the transition table, or you modify transition tables such that each transition points at the first transition of the target state.

Re:Node.js in Flame Graphs by andrewvc on Wednesday November 19, 2014 @12:37PM (#8631370)
A lot of people here are right, the right way is with an NFA. I just want to add that the solution is not even hard, you can do it with string concatenation and capture groups using regexps. Regexps are NFAs, and are highly optimized C code in just about every JS engine.

If I have the routes /foo/bar and /foo/bar/(\d+) I can generate the regexp ((^\/foo\/bar$)|(^\/foo\/bar\/\d+$))

I'm not at all surprised, the quality of software in node is pretty low, I've seen numerous issues in node libs being just as boneheaded. I swear, the fact that the express devs overlooked a key optimization is crazy. Rails, by way of example, uses the Journey engine to solve this problem (https://github.com/rails/journey)

Re:Node.js in Flame Graphs by thedufer on Wednesday November 19, 2014 @01:11PM (#8631580)
> I can generate the regexp ((^\/foo\/bar$)|(^\/foo\/bar\/\d+$))

And how would you know which one got matched? The regex match isn't going to tell you that. Also, it needs to recognize if multiple were matched, which is definitely not going to be done by the built-in regex matcher.

It's certainly possible, but pretending it's trivial isn't helping, either.

Re:Node.js in Flame Graphs by blinks on Wednesday November 19, 2014 @01:35PM (#8631732)
For more concrete syntax, consider the following Python:

  >>> import re
  >>> route = re.compile(r'(?P<fb>^/foo/bar$)|(?P<fbd>^/foo/bar/\d+$)')
  >>> route.match('/foo/bar').groupdict()
  {'fb': '/foo/bar', 'fbd': None}
  >>> route.match('/foo/bar/1').groupdict()
  {'fb': None, 'fbd': '/foo/bar/1'}
If the fb group is set, act on the first route. If the fbd group is set, act on the second.
Re:Node.js in Flame Graphs by rudolf0 on Wednesday November 19, 2014 @05:27PM (#8633090)
I know very little of NFAs/DFAs/FSMs, or even string parsing in general, but a year ago I built a URL matching engine using exactly this method in Python, in combination with Google's RE2 library (https://code.google.com/p/re2/). It was far faster than anything else I experimented with, and RE2 also improved the speed dramatically by eliminating backtracking.

Nice to know that what I made is considered the best solution algorithmically.

Re:Node.js in Flame Graphs by FrozenCow on Wednesday November 19, 2014 @01:42PM (#8631767)
You can check the 2nd and 3rd groups when using /(^\/foo\/bar$)|(^\/foo\/bar\/\d+$)/:

    > /(^\/foo\/bar$)|(^\/foo\/bar\/\d+$)/.exec('/foo/bar')
    [ '/foo/bar',
      '/foo/bar',
      undefined,
      index: 0,
      input: '/foo/bar' ]

    > /(^\/foo\/bar$)|(^\/foo\/bar\/\d+$)/.exec('/foo/bar/3')
    [ '/foo/bar/3',
      undefined,
      '/foo/bar/3',
      index: 0,
      input: '/foo/bar/3' ]
EDIT: Markdown differences.
Re:Node.js in Flame Graphs by andrewvc on Wednesday November 19, 2014 @01:35PM (#8631730)
First off, you don't need to handle the 'multiple' case, since the "|" has precedence rules applied to it. The leftmost match is the match. Second, you know which one matched based on the index of the capture groups, which is deterministic. See this example: http://rubular.com/r/HiW6gjnURe

You could write a simple router based on this in < 50 lines of JS. I'd do it now, but I have work to do.

Re:Node.js in Flame Graphs by thedufer on Wednesday November 19, 2014 @01:44PM (#8631778)
> First off, you don't need to handle the 'multiple' case, since the "|" has precedence rules applied to it.

Read my top-level post again - multiple routes can be called on the same request, so express has to be able to find all of the matches, not just the first one. This is a mistake in the original article, as the author doesn't appear to understand the power of express routers.

> Second, you know which one matched based on the index of the capture groups, which is deterministic.

Deterministic, yes. But now you're also writing your own regex parser, because you have to know how many capture groups each sub-regex contains in order to figure out the indexing.

Re:Node.js in Flame Graphs by andrewvc on Wednesday November 19, 2014 @02:01PM (#8631900)
Ah, I did miss that, but it does still work. Run the following code below, you'll see multiple groups match.

    "/foo/bar/3".match(/((^\/foo\/bar\/.*)|(^\/foo\/bar\/(\d+)$)|(^\/baz))/) => 
Additionally, parsing out the number of capture groups in a regexp is simple, just looked for unescaped paren groups. You can do it once at route definition.
Re:Node.js in Flame Graphs by thedufer on Wednesday November 19, 2014 @03:06PM (#8632287)
> just looked for unescaped paren groups

Don't forget non-matching groups! Which brings up my point yet again - everyone is pretending this is a dead-simple problem to solve, but all of the proposed solutions are buggy. Not really an option for such a widely-used library.

I'm not claiming it's impossible. I'm claiming it's more difficult than anyone is willing to admit.

Re:Node.js in Flame Graphs by boblauer on Thursday November 20, 2014 @04:34PM (#8638295)
Counting capturing groups is a solved problem - http://stackoverflow.com/questions/16046620/regex-to-count-t...
Re:Node.js in Flame Graphs by breuleux on Wednesday November 19, 2014 @02:13PM (#8631990)
Actually, no: if you run this you will see three matching groups, but the first is always the whole match (this is in addition to the capture groups that are defined), the second is the (redundant) capture group you put around the whole thing, and the third is foo/bar/.*. /foo/bar/\d+ is not matched at all, even though it matches the input string.
Re:Node.js in Flame Graphs by andrewvc on Wednesday November 19, 2014 @02:31PM (#8632100)
You are 100% correct, I forgot about that extra group (that doesn't need to be there).

That being said, most frameworks don't have multiple matching routes, and I can't imagine that many people miss them. I've never used a framework that supported that, and I can't say I've ever felt I've missed out. Middleware is the right approach to that problem generally.

Re:Node.js in Flame Graphs by thedufer on Wednesday November 19, 2014 @03:04PM (#8632273)
The point is to be able to have middleware that only runs on certain urls. For example, you might have middleware that only runs on a particular version of your API (`regexp: ^/1\/.*/`). I've used it fairly extensively.

The point being, now you're describing a completely different, less powerful framework. Which, yes, of course it's faster. You'll notice that's what Netflix ended up doing - they moved to `restify` from `express`.

You might as well just only allow routes to be strings and then you have the constant-time map implementation that the article describes as the expected implementation.

Re:Node.js in Flame Graphs by breuleux on Wednesday November 19, 2014 @01:35PM (#8631735)
The match object will be of the form [undefined, ..., input_string, undefined, ...], so you can use something like m.indexOf(input_string) to find out the index. Unfortunately that's O(n) again, unless the engine uses a sparse array implementation...
Re:Node.js in Flame Graphs by andrewvc on Wednesday November 19, 2014 @01:46PM (#8631788)
You're right that it's O(n), but it's still faster than express js. It's not iterating through the array that's slow, its evaluating the members of that array. Express must check each route using JS functions that each execute a regexp match per route. Using a single regexp is much faster because 1.) by unioning the regexps you get the correct NFA, which will run much faster 2.) there's less function call overhead.

Iterating through the array looking for the first non-null value, then looking it up in a JS object O(1) matching capture group positions to routes is much faster than having to dispatch a bunch of functions. Additionally, it can be JITed to much faster code I would wager.

So, expressJS = N regexp matches, while this method = 1 regexp match + N null checks in the resulting array + 1 hashmap lookup in an object to return the proper route. The second solution is going to be much faster, even for large routing tables, esp. given how optimizable that array lookup code is.

Re:Node.js in Flame Graphs by boblauer on Thursday November 20, 2014 @04:32PM (#8638284)
You can get the index of the match via the .exec function. It returns an array. The first element is the match, and then an element for each capturing group. You'd need to keep track of how many sub-capturing groups each individual top-level capturing group has, but that's not terribly difficult.

As far as multiple matches, after you found a match, you would then test against a new regex that is everything to the right of the previous regex's captured group.

((a)|(b)|(c)|(d)) -> if b matches, you then test against ((c)|(d)), and so on.

Re:Node.js in Flame Graphs by spion on Wednesday November 19, 2014 @04:47PM (#8632819)
This indicates it probably wont be much faster in JS

http://jsperf.com/regex-list-vs-full

Most of the time in the single-regex case is likely spent on allocating the matches array (which for a router with 300 routes would have over 300 undefined elements on every execution)

V8 is not quite the typical dynamic language runtime. Code that would be dead slow in others is optimized really well by the amazing JIT.

Re:Node.js in Flame Graphs by andrewvc on Wednesday November 19, 2014 @05:00PM (#8632917)
Interesting, I'm looking at it right now, and it's saying that the multiple regexes are ~ 50% slower in both chrome and firefox. Am I missing something? It looks like the single regex actually is a significant win.

It is interesting how much allocation changes things though, very cool that you made this.

Re:Node.js in Flame Graphs by spion on Wednesday November 19, 2014 @05:05PM (#8632960)
Yeah multiple regexes are slower, but the performance gain isn't too gigantic. If we had `regex.execIterable(string)` we could avoid allocating massive arrays and maybe get a really significant difference
Re:Node.js in Flame Graphs by llimllib on Wednesday November 19, 2014 @12:47PM (#8631431)
Regexps with backtracks are not regular languages (IIRC, right?), does that matter here?
Re:Node.js in Flame Graphs by stepstep on Thursday November 20, 2014 @01:10AM (#8634632)
That's right. Typical routing regexes will not use backreferences, so that's not really an issue here. However, most routes do have parameters implemented as capture groups (which, I believe, is also not technically a feature of regular expressions). One simple solution would be to use a big regex (the union of all the routes) to determine which route it is (in O(n) time), and then once you know the route, use another regex to parse the parameters in the URL. So each route lookup requires 2 regex matches rather than N.
Re:Node.js in Flame Graphs by stepstep on Thursday November 20, 2014 @08:06AM (#8635505)
Just re-read this and realized it's unclear: when I said O(n) time, I meant linear in the length of the URL to be parsed. The point is that with this technique, it doesn't matter how many routes there are.
Re:Node.js in Flame Graphs by plorkyeran on Wednesday November 19, 2014 @01:15PM (#8631609)
Given that combined regex, how do you determine which of the routes that went in to it was the one that matched?
Re:Node.js in Flame Graphs by baudehlo on Wednesday November 19, 2014 @01:28PM (#8631683)
If it matched the first, match[2] is set. If it matched the second, match[3] is set.

Or you use a better regexp library than the default JS one and use named captures.

Re:Node.js in Flame Graphs by mjs on Wednesday November 19, 2014 @12:59PM (#8631504)
It looks like routes are either strings (exact match) or regular expression objects. I don't think there's a way to combine regular expressions in JavaScript.

I don't think this was an unreasonable implementation. Sure, it could be optimized, but no-one needed it enough to do the work. (Even Netflix didn't need a faster matcher, they needed a matcher that didn't slow down between the 1st match and the nth.)

Re:Node.js in Flame Graphs by andrewvc on Wednesday November 19, 2014 @01:04PM (#8631542)
What I'm saying is that the pipe (or) "|" is a cheap way to combine regexps. It has its limitations, but its better than an O(n) search through an array. The regexp parser can create an NFA that is far more optimized.
Re:Node.js in Flame Graphs by spion on Wednesday November 19, 2014 @05:00PM (#8632919)
In JS, "far more" isn't quite that dramatically far as in other dynamic language runtimes. See http://jsperf.com/regex-list-vs-full
Re:Node.js in Flame Graphs by LunaSea on Wednesday November 19, 2014 @05:43PM (#8633179)
Oh yes, I forgot about the legendary Ruby On Rails code quality: http://puppetlabs.com/security/cve/cve-2013-0277
Re:Node.js in Flame Graphs by koenigdavidmj on Wednesday November 19, 2014 @12:25PM (#8631284)
There is a library (I think it is Google's re2) that supports throwing a bunch of regular expressions into a single structure, matching an input string against all of them at once, and answering which patterns matched the input string. This gets you route lookup in time linear to the input string (or if not linear, still better than checking all the patterns one after another).
Re:Node.js in Flame Graphs by rudolf0 on Wednesday November 19, 2014 @05:31PM (#8633108)
I use RE2 and currently achieve this with regular string concatenation. Do you know if they provide an API for doing this without concatenation, and if it would be faster?
Re:Node.js in Flame Graphs by koenigdavidmj on Wednesday November 19, 2014 @05:50PM (#8633211)
The header file is available at https://code.google.com/p/re2/source/browse/re2/set.h , and if I recall correctly, it merges all the regexes into one DFA so that it's faster than just concatenating all your patterns into one string.
Re:Node.js in Flame Graphs by f2f on Wednesday November 19, 2014 @02:04PM (#8631923)
Re:Node.js in Flame Graphs by andrewstuart2 on Wednesday November 19, 2014 @01:58PM (#8631869)
The other thing you can't properly get using a map is precedence. By building an array you can go through it in order so that middleware is run in the order it was defined in the code.

That way, if you have any route definitions that match early on, especially if they allow execution to continue ( by calling next() ), they will run before any following code does.

This allows your code to be very expressive and intuitive when reading it, as things have a defined order.

I imagine the recursion is also done so that you can use a route object anywhere in that ordered stack and it will apply all its own handlers, in the order they were defined in the code.

Re:Node.js in Flame Graphs by ksherlock on Wednesday November 19, 2014 @12:26PM (#8631288)
You could compile all the regular expressions into a table. Lex has been doing that since, oh, 1975.
Re:Node.js in Flame Graphs by cdoxsey on Wednesday November 19, 2014 @02:02PM (#8631902)
Most routes are prefix matches, so a trie (or one of its variants) would work well.
Re:Node.js in Flame Graphs by cataphract on Wednesday November 19, 2014 @12:18PM (#8631240)
On the other hand, without the recursion I don't see how the flame graph would have been helpful.
Re:Node.js in Flame Graphs by msmillie on Wednesday November 19, 2014 @12:38PM (#8631376)
In flame graphs heavy iteration tends to give "wider" structures. Recall the stacks aren't ordered on the X axis by time, but by contents.