Add option to node bindings to return result as a pre-generated JSON string (this avoids a lot of overhead, and moves JSON string rendering out of the main event loop).

This commit is contained in:
Daniel Patterson 2018-09-01 22:22:37 -07:00 committed by Daniel Patterson
parent 85515f063a
commit 0971f06193
9 changed files with 287 additions and 20 deletions

View File

@ -3,6 +3,7 @@
- Optimizations: - Optimizations:
- CHANGED: Map matching is now almost twice as fast. [#5060](https://github.com/Project-OSRM/osrm-backend/pull/5060) - CHANGED: Map matching is now almost twice as fast. [#5060](https://github.com/Project-OSRM/osrm-backend/pull/5060)
- CHANGED: Use Grisu2 for serializing floating point numbers. [#5188](https://github.com/Project-OSRM/osrm-backend/pull/5188) - CHANGED: Use Grisu2 for serializing floating point numbers. [#5188](https://github.com/Project-OSRM/osrm-backend/pull/5188)
- ADDED: Node bindings can return pre-rendered JSON buffer. [#5189](https://github.com/Project-OSRM/osrm-backend/pull/5189)
- Bugfixes: - Bugfixes:
- FIXED: collapsing of ExitRoundabout instructions [#5114](https://github.com/Project-OSRM/osrm-backend/issues/5114) - FIXED: collapsing of ExitRoundabout instructions [#5114](https://github.com/Project-OSRM/osrm-backend/issues/5114)
- FIXED: negative distances in table plugin annotation [#5106](https://github.com/Project-OSRM/osrm-backend/issues/5106) - FIXED: negative distances in table plugin annotation [#5106](https://github.com/Project-OSRM/osrm-backend/issues/5106)

View File

@ -297,6 +297,29 @@ Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refer
2) `waypoint_index`: index of the point in the trip. 2) `waypoint_index`: index of the point in the trip.
**`trips`**: an array of [`Route`](#route) objects that assemble the trace. **`trips`**: an array of [`Route`](#route) objects that assemble the trace.
## Plugin behaviour
All plugins support a second additional object that is available to configure some NodeJS specific behaviours.
- `plugin_config` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Object literal containing parameters for the trip query.
- `plugin_config.format` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** The format of the result object to various API calls. Valid options are `object` (default), which returns a standard Javascript object, as described above, and `json_buffer`, which will return a NodeJS **[Buffer](https://nodejs.org/api/buffer.html)** object, containing a JSON string. The latter has the advantage that it can be immediately serialized to disk/sent over the network, and the generation of the string is performed outside the main NodeJS event loop. This option is ignored by the `tile` plugin.
**Examples**
```javascript
var osrm = new OSRM('network.osrm');
var options = {
coordinates: [
[13.36761474609375, 52.51663871100423],
[13.374481201171875, 52.506191342034576]
]
};
osrm.route(options, { format: "json_buffer" }, function(err, response) {
if (err) throw err;
console.log(response.toString("utf-8"));
});
```
## Responses ## Responses
Responses Responses

View File

@ -2,6 +2,7 @@
#define OSRM_BINDINGS_NODE_SUPPORT_HPP #define OSRM_BINDINGS_NODE_SUPPORT_HPP
#include "nodejs/json_v8_renderer.hpp" #include "nodejs/json_v8_renderer.hpp"
#include "util/json_renderer.hpp"
#include "osrm/approach.hpp" #include "osrm/approach.hpp"
#include "osrm/bearing.hpp" #include "osrm/bearing.hpp"
@ -24,6 +25,7 @@
#include <algorithm> #include <algorithm>
#include <iostream> #include <iostream>
#include <iterator> #include <iterator>
#include <sstream>
#include <string> #include <string>
#include <vector> #include <vector>
@ -42,6 +44,13 @@ using match_parameters_ptr = std::unique_ptr<osrm::MatchParameters>;
using nearest_parameters_ptr = std::unique_ptr<osrm::NearestParameters>; using nearest_parameters_ptr = std::unique_ptr<osrm::NearestParameters>;
using table_parameters_ptr = std::unique_ptr<osrm::TableParameters>; using table_parameters_ptr = std::unique_ptr<osrm::TableParameters>;
struct PluginParameters
{
bool renderJSONToBuffer = false;
};
using ObjectOrString = typename mapbox::util::variant<osrm::json::Object, std::string>;
template <typename ResultT> inline v8::Local<v8::Value> render(const ResultT &result); template <typename ResultT> inline v8::Local<v8::Value> render(const ResultT &result);
template <> v8::Local<v8::Value> inline render(const std::string &result) template <> v8::Local<v8::Value> inline render(const std::string &result)
@ -49,11 +58,21 @@ template <> v8::Local<v8::Value> inline render(const std::string &result)
return Nan::CopyBuffer(result.data(), result.size()).ToLocalChecked(); return Nan::CopyBuffer(result.data(), result.size()).ToLocalChecked();
} }
template <> v8::Local<v8::Value> inline render(const osrm::json::Object &result) template <> v8::Local<v8::Value> inline render(const ObjectOrString &result)
{ {
v8::Local<v8::Value> value; if (result.is<osrm::json::Object>())
renderToV8(value, result); {
return value; // Convert osrm::json object tree into matching v8 object tree
v8::Local<v8::Value> value;
renderToV8(value, result.get<osrm::json::Object>());
return value;
}
else
{
// Return the string object as a node Buffer
return Nan::CopyBuffer(result.get<std::string>().data(), result.get<std::string>().size())
.ToLocalChecked();
}
} }
inline void ParseResult(const osrm::Status &result_status, osrm::json::Object &result) inline void ParseResult(const osrm::Status &result_status, osrm::json::Object &result)
@ -814,6 +833,50 @@ inline bool parseCommonParameters(const v8::Local<v8::Object> &obj, ParamType &p
return true; return true;
} }
inline PluginParameters
argumentsToPluginParameters(const Nan::FunctionCallbackInfo<v8::Value> &args)
{
if (args.Length() < 3 || !args[1]->IsObject())
{
return {};
}
v8::Local<v8::Object> obj = Nan::To<v8::Object>(args[1]).ToLocalChecked();
if (obj->Has(Nan::New("format").ToLocalChecked()))
{
v8::Local<v8::Value> format = obj->Get(Nan::New("format").ToLocalChecked());
if (format.IsEmpty())
{
return {};
}
if (!format->IsString())
{
Nan::ThrowError("format must be a string: \"object\" or \"json_buffer\"");
return {};
}
const Nan::Utf8String format_utf8str(format);
std::string format_str{*format_utf8str, *format_utf8str + format_utf8str.length()};
if (format_str == "object")
{
return {false};
}
else if (format_str == "json_buffer")
{
return {true};
}
else
{
Nan::ThrowError("format must be a string: \"object\" or \"json_buffer\"");
return {};
}
}
return {};
}
inline route_parameters_ptr inline route_parameters_ptr
argumentsToRouteParameter(const Nan::FunctionCallbackInfo<v8::Value> &args, argumentsToRouteParameter(const Nan::FunctionCallbackInfo<v8::Value> &args,
bool requires_multiple_coordinates) bool requires_multiple_coordinates)
@ -1357,6 +1420,6 @@ argumentsToMatchParameter(const Nan::FunctionCallbackInfo<v8::Value> &args,
return params; return params;
} }
} // ns node_osrm } // namespace node_osrm
#endif #endif

View File

@ -9,12 +9,15 @@
#include "osrm/trip_parameters.hpp" #include "osrm/trip_parameters.hpp"
#include <exception> #include <exception>
#include <sstream>
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
#include "nodejs/node_osrm.hpp" #include "nodejs/node_osrm.hpp"
#include "nodejs/node_osrm_support.hpp" #include "nodejs/node_osrm_support.hpp"
#include "util/json_renderer.hpp"
namespace node_osrm namespace node_osrm
{ {
@ -122,6 +125,8 @@ inline void async(const Nan::FunctionCallbackInfo<v8::Value> &info,
if (!params) if (!params)
return; return;
auto pluginParams = argumentsToPluginParameters(info);
BOOST_ASSERT(params->IsValid()); BOOST_ASSERT(params->IsValid());
if (!info[info.Length() - 1]->IsFunction()) if (!info[info.Length() - 1]->IsFunction())
@ -137,9 +142,89 @@ inline void async(const Nan::FunctionCallbackInfo<v8::Value> &info,
Worker(std::shared_ptr<osrm::OSRM> osrm_, Worker(std::shared_ptr<osrm::OSRM> osrm_,
ParamPtr params_, ParamPtr params_,
ServiceMemFn service, ServiceMemFn service,
Nan::Callback *callback) Nan::Callback *callback,
PluginParameters pluginParams_)
: Base(callback), osrm{std::move(osrm_)}, service{std::move(service)}, : Base(callback), osrm{std::move(osrm_)}, service{std::move(service)},
params{std::move(params_)} params{std::move(params_)}, pluginParams{std::move(pluginParams_)}
{
}
void Execute() override try
{
osrm::json::Object r;
const auto status = ((*osrm).*(service))(*params, r);
ParseResult(status, r);
if (pluginParams.renderJSONToBuffer)
{
std::ostringstream buf;
osrm::util::json::render(buf, r);
result = buf.str();
}
else
{
result = r;
}
}
catch (const std::exception &e)
{
SetErrorMessage(e.what());
}
void HandleOKCallback() override
{
Nan::HandleScope scope;
const constexpr auto argc = 2u;
v8::Local<v8::Value> argv[argc] = {Nan::Null(), render(result)};
callback->Call(argc, argv);
}
// Keeps the OSRM object alive even after shutdown until we're done with callback
std::shared_ptr<osrm::OSRM> osrm;
ServiceMemFn service;
const ParamPtr params;
const PluginParameters pluginParams;
ObjectOrString result;
};
auto *callback = new Nan::Callback{info[info.Length() - 1].As<v8::Function>()};
Nan::AsyncQueueWorker(
new Worker{self->this_, std::move(params), service, callback, std::move(pluginParams)});
}
template <typename ParameterParser, typename ServiceMemFn>
inline void asyncForTiles(const Nan::FunctionCallbackInfo<v8::Value> &info,
ParameterParser argsToParams,
ServiceMemFn service,
bool requires_multiple_coordinates)
{
auto params = argsToParams(info, requires_multiple_coordinates);
if (!params)
return;
auto pluginParams = argumentsToPluginParameters(info);
BOOST_ASSERT(params->IsValid());
if (!info[info.Length() - 1]->IsFunction())
return Nan::ThrowTypeError("last argument must be a callback function");
auto *const self = Nan::ObjectWrap::Unwrap<Engine>(info.Holder());
using ParamPtr = decltype(params);
struct Worker final : Nan::AsyncWorker
{
using Base = Nan::AsyncWorker;
Worker(std::shared_ptr<osrm::OSRM> osrm_,
ParamPtr params_,
ServiceMemFn service,
Nan::Callback *callback,
PluginParameters pluginParams_)
: Base(callback), osrm{std::move(osrm_)}, service{std::move(service)},
params{std::move(params_)}, pluginParams{std::move(pluginParams_)}
{ {
} }
@ -167,18 +252,14 @@ inline void async(const Nan::FunctionCallbackInfo<v8::Value> &info,
std::shared_ptr<osrm::OSRM> osrm; std::shared_ptr<osrm::OSRM> osrm;
ServiceMemFn service; ServiceMemFn service;
const ParamPtr params; const ParamPtr params;
const PluginParameters pluginParams;
// All services return json::Object .. except for Tile! std::string result;
using ObjectOrString =
typename std::conditional<std::is_same<ParamPtr, tile_parameters_ptr>::value,
std::string,
osrm::json::Object>::type;
ObjectOrString result;
}; };
auto *callback = new Nan::Callback{info[info.Length() - 1].As<v8::Function>()}; auto *callback = new Nan::Callback{info[info.Length() - 1].As<v8::Function>()};
Nan::AsyncQueueWorker(new Worker{self->this_, std::move(params), service, callback}); Nan::AsyncQueueWorker(
new Worker{self->this_, std::move(params), service, callback, std::move(pluginParams)});
} }
// clang-format off // clang-format off
@ -341,7 +422,7 @@ NAN_METHOD(Engine::table) //
// clang-format on // clang-format on
NAN_METHOD(Engine::tile) NAN_METHOD(Engine::tile)
{ {
async(info, &argumentsToTileParameters, &osrm::OSRM::Tile, {/*unused*/}); asyncForTiles(info, &argumentsToTileParameters, &osrm::OSRM::Tile, {/*unused*/});
} }
// clang-format off // clang-format off

View File

@ -25,6 +25,28 @@ test('match: match in Monaco', function(assert) {
}); });
}); });
test('match: match in Monaco returning a buffer', function(assert) {
assert.plan(6);
var osrm = new OSRM(data_path);
var options = {
coordinates: three_test_coordinates,
timestamps: [1424684612, 1424684616, 1424684620]
};
osrm.match(options, { format: 'json_buffer' }, function(err, response) {
assert.ifError(err);
assert.ok(response instanceof Buffer);
response = JSON.parse(response);
assert.equal(response.matchings.length, 1);
assert.ok(response.matchings.every(function(m) {
return !!m.distance && !!m.duration && Array.isArray(m.legs) && !!m.geometry && m.confidence > 0;
}))
assert.equal(response.tracepoints.length, 3);
assert.ok(response.tracepoints.every(function(t) {
return !!t.hint && !isNaN(t.matchings_index) && !isNaN(t.waypoint_index) && !!t.name;
}));
});
});
test('match: match in Monaco without timestamps', function(assert) { test('match: match in Monaco without timestamps', function(assert) {
assert.plan(3); assert.plan(3);
var osrm = new OSRM(data_path); var osrm = new OSRM(data_path);
@ -225,6 +247,16 @@ test('match: throws on invalid tidy param', function(assert) {
/tidy must be of type Boolean/); /tidy must be of type Boolean/);
}); });
test('match: throws on invalid config param', function(assert) {
assert.plan(1);
var osrm = new OSRM({path: mld_data_path, algorithm: 'MLD'});
var options = {
coordinates: three_test_coordinates,
};
assert.throws(function() { osrm.match(options, { format: 'invalid' }, function(err, response) {}) },
/format must be a string:/);
});
test('match: match in Monaco without motorways', function(assert) { test('match: match in Monaco without motorways', function(assert) {
assert.plan(3); assert.plan(3);
var osrm = new OSRM({path: mld_data_path, algorithm: 'MLD'}); var osrm = new OSRM({path: mld_data_path, algorithm: 'MLD'});

View File

@ -19,6 +19,21 @@ test('nearest', function(assert) {
}); });
}); });
test('nearest', function(assert) {
assert.plan(5);
var osrm = new OSRM(data_path);
osrm.nearest({
coordinates: [three_test_coordinates[0]]
}, { format: 'json_buffer' }, function(err, result) {
assert.ifError(err);
assert.ok(result instanceof Buffer);
result = JSON.parse(result);
assert.equal(result.waypoints.length, 1);
assert.equal(result.waypoints[0].location.length, 2);
assert.ok(result.waypoints[0].hasOwnProperty('name'));
});
});
test('nearest: can ask for multiple nearest pts', function(assert) { test('nearest: can ask for multiple nearest pts', function(assert) {
assert.plan(2); assert.plan(2);
var osrm = new OSRM(data_path); var osrm = new OSRM(data_path);
@ -32,7 +47,7 @@ test('nearest: can ask for multiple nearest pts', function(assert) {
}); });
test('nearest: throws on invalid args', function(assert) { test('nearest: throws on invalid args', function(assert) {
assert.plan(6); assert.plan(7);
var osrm = new OSRM(data_path); var osrm = new OSRM(data_path);
var options = {}; var options = {};
assert.throws(function() { osrm.nearest(options); }, assert.throws(function() { osrm.nearest(options); },
@ -52,6 +67,10 @@ test('nearest: throws on invalid args', function(assert) {
options.number = 0; options.number = 0;
assert.throws(function() { osrm.nearest(options, function(err, res) {}); }, assert.throws(function() { osrm.nearest(options, function(err, res) {}); },
/Number must be an integer greater than or equal to 1/); /Number must be an integer greater than or equal to 1/);
options.number = 1;
assert.throws(function() { osrm.nearest(options, { format: 'invalid' }, function(err, res) {}); },
/format must be a string:/);
}); });
test('nearest: nearest in Monaco without motorways', function(assert) { test('nearest: nearest in Monaco without motorways', function(assert) {

View File

@ -43,8 +43,22 @@ test('route: routes Monaco on CoreCH', function(assert) {
}); });
}); });
test('route: routes Monaco and returns a JSON buffer', function(assert) {
assert.plan(6);
var osrm = new OSRM({path: monaco_corech_path, algorithm: 'CoreCH'});
osrm.route({coordinates: [[13.43864,52.51993],[13.415852,52.513191]]}, { format: 'json_buffer'}, function(err, result) {
assert.ifError(err);
assert.ok(result instanceof Buffer);
const route = JSON.parse(result);
assert.ok(route.waypoints);
assert.ok(route.routes);
assert.ok(route.routes.length);
assert.ok(route.routes[0].geometry);
});
});
test('route: throws with too few or invalid args', function(assert) { test('route: throws with too few or invalid args', function(assert) {
assert.plan(3); assert.plan(4);
var osrm = new OSRM(monaco_path); var osrm = new OSRM(monaco_path);
assert.throws(function() { osrm.route({coordinates: two_test_coordinates}) }, assert.throws(function() { osrm.route({coordinates: two_test_coordinates}) },
/Two arguments required/); /Two arguments required/);
@ -52,6 +66,8 @@ test('route: throws with too few or invalid args', function(assert) {
/First arg must be an object/); /First arg must be an object/);
assert.throws(function() { osrm.route({coordinates: two_test_coordinates}, true)}, assert.throws(function() { osrm.route({coordinates: two_test_coordinates}, true)},
/last argument must be a callback function/); /last argument must be a callback function/);
assert.throws(function() { osrm.route({coordinates: two_test_coordinates}, { format: 'invalid' }, function(err, route) {})},
/format must be a string:/);
}); });
test('route: provides no alternatives by default, but when requested it may (not guaranteed)', function(assert) { test('route: provides no alternatives by default, but when requested it may (not guaranteed)', function(assert) {

View File

@ -48,6 +48,20 @@ test('table: test annotations paramater combination', function(assert) {
}); });
}); });
test('table: returns buffer', function(assert) {
assert.plan(3);
var osrm = new OSRM(data_path);
var options = {
coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
};
osrm.table(options, { format: 'json_buffer' }, function(err, table) {
assert.ifError(err);
assert.ok(table instanceof Buffer);
table = JSON.parse(table);
assert.ok(table['durations'], 'distances table result should exist');
});
});
var tables = ['distances', 'durations']; var tables = ['distances', 'durations'];
tables.forEach(function(annotation) { tables.forEach(function(annotation) {
@ -116,7 +130,7 @@ tables.forEach(function(annotation) {
}); });
test('table: ' + annotation + ' throws on invalid arguments', function(assert) { test('table: ' + annotation + ' throws on invalid arguments', function(assert) {
assert.plan(14); assert.plan(15);
var osrm = new OSRM(data_path); var osrm = new OSRM(data_path);
var options = {annotations: [annotation.slice(0,-1)]}; var options = {annotations: [annotation.slice(0,-1)]};
assert.throws(function() { osrm.table(options); }, assert.throws(function() { osrm.table(options); },
@ -135,6 +149,9 @@ tables.forEach(function(annotation) {
/Coordinates must be an array of \(lon\/lat\) pairs/); /Coordinates must be an array of \(lon\/lat\) pairs/);
options.coordinates = two_test_coordinates; options.coordinates = two_test_coordinates;
assert.throws(function() { osrm.table(options, { format: 'invalid' }, function(err, response) {}) },
/format must be a string:/);
options.sources = true; options.sources = true;
assert.throws(function() { osrm.table(options, function(err, response) {}) }, assert.throws(function() { osrm.table(options, function(err, response) {}) },
/Sources must be an array of indices \(or undefined\)/); /Sources must be an array of indices \(or undefined\)/);

View File

@ -17,6 +17,19 @@ test('trip: trip in Monaco', function(assert) {
}); });
}); });
test('trip: trip in Monaco as a buffer', function(assert) {
assert.plan(3);
var osrm = new OSRM(data_path);
osrm.trip({coordinates: two_test_coordinates}, { format: 'json_buffer' }, function(err, trip) {
assert.ifError(err);
assert.ok(trip instanceof Buffer);
trip = JSON.parse(trip);
for (t = 0; t < trip.trips.length; t++) {
assert.ok(trip.trips[t].geometry);
}
});
});
test('trip: trip with many locations in Monaco', function(assert) { test('trip: trip with many locations in Monaco', function(assert) {
assert.plan(2); assert.plan(2);
@ -33,12 +46,14 @@ test('trip: trip with many locations in Monaco', function(assert) {
}); });
test('trip: throws with too few or invalid args', function(assert) { test('trip: throws with too few or invalid args', function(assert) {
assert.plan(2); assert.plan(3);
var osrm = new OSRM(data_path); var osrm = new OSRM(data_path);
assert.throws(function() { osrm.trip({coordinates: two_test_coordinates}) }, assert.throws(function() { osrm.trip({coordinates: two_test_coordinates}) },
/Two arguments required/); /Two arguments required/);
assert.throws(function() { osrm.trip(null, function(err, trip) {}) }, assert.throws(function() { osrm.trip(null, function(err, trip) {}) },
/First arg must be an object/); /First arg must be an object/);
assert.throws(function() { osrm.trip({coordinates: two_test_coordinates}, { format: 'invalid' }, function(err, trip) {}) },
/format must be a string:/);
}); });
test('trip: throws with bad params', function(assert) { test('trip: throws with bad params', function(assert) {