server.dart 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import 'dart:async' show Future;
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'dart:isolate';
  5. import 'dart:math' show Random;
  6. import 'package:args/args.dart' show ArgParser;
  7. import 'package:mustache/mustache.dart' as mustache;
  8. import 'package:postgresql/postgresql.dart' as pg;
  9. import 'package:postgresql/postgresql_pool.dart' as pgpool;
  10. import 'package:yaml/yaml.dart' as yaml;
  11. /// Starts a new HTTP server that implements the tests to be benchmarked. The
  12. /// address and port for incoming connections is configurable via command line
  13. /// arguments, as is the number of database connections to be maintained in the
  14. /// connection pool.
  15. void main(List<String> args) {
  16. var parser = new ArgParser();
  17. parser.addOption('address', abbr: 'a', defaultsTo: '0.0.0.0');
  18. parser.addOption('port', abbr: 'p', defaultsTo: '8080');
  19. parser.addOption('dbconnections', abbr: 'd', defaultsTo: '256');
  20. parser.addOption('isolates', abbr: 'i', defaultsTo: '1');
  21. var arguments = parser.parse(args);
  22. var isolates = int.parse(arguments['isolates']);
  23. var dbConnections = int.parse(arguments['dbconnections']) ~/ isolates;
  24. ServerSocket.bind(arguments['address'], int.parse(arguments['port'])).then(
  25. (server) {
  26. var ref = server.reference;
  27. for (int i = 1; i < isolates; i++) {
  28. Isolate.spawn(startInIsolate, [ref, dbConnections]);
  29. }
  30. _startServer(server, dbConnections);
  31. });
  32. }
  33. void startInIsolate(List args) {
  34. var ref = args[0];
  35. var dbConnections = args[1];
  36. ref.create().then((server) {
  37. _startServer(server, dbConnections);
  38. });
  39. }
  40. /// The entity used in the database query and update tests.
  41. class World {
  42. int id;
  43. int randomnumber;
  44. World(this.id, this.randomnumber);
  45. toJson() => {'id': id, 'randomNumber': randomnumber};
  46. }
  47. /// The entity used in the fortunes test.
  48. class Fortune implements Comparable<Fortune> {
  49. int id;
  50. String message;
  51. Fortune(this.id, this.message);
  52. int compareTo(Fortune other) => message.compareTo(other.message);
  53. }
  54. /// The number of rows in the world entity table.
  55. const _WORLD_TABLE_SIZE = 10000;
  56. /// A random number generator.
  57. final _RANDOM = new Random();
  58. /// The PostgreSQL connection pool used by all the tests that require database
  59. /// connectivity.
  60. pgpool.Pool _connectionPool;
  61. /// The mustache template which is rendered in the fortunes test.
  62. mustache.Template _fortunesTemplate;
  63. /// Starts a benchmark server, which listens for connections from
  64. /// '[address] : [port]' and maintains [dbConnections] connections to the
  65. /// database.
  66. void _startServer(serverSocket, dbConnections) {
  67. Future.wait([
  68. new File('postgresql.yaml').readAsString().then((config) {
  69. _connectionPool = new pgpool.Pool(
  70. new pg.Settings.fromMap(yaml.loadYaml(config)).toUri(),
  71. min: dbConnections, max: dbConnections);
  72. return _connectionPool.start();
  73. }),
  74. new File('fortunes.mustache').readAsString().then((template) {
  75. _fortunesTemplate = mustache.parse(template);
  76. })
  77. ]).then((_) {
  78. var server = new HttpServer.listenOn(serverSocket);
  79. server.defaultResponseHeaders.clear();
  80. server.serverHeader = 'dart';
  81. server.listen((request) {
  82. switch (request.uri.path) {
  83. case '/json':
  84. _jsonTest(request);
  85. break;
  86. case '/db':
  87. _dbTest(request);
  88. break;
  89. case '/queries':
  90. _queriesTest(request);
  91. break;
  92. case '/fortunes':
  93. _fortunesTest(request);
  94. break;
  95. case '/updates':
  96. _updatesTest(request);
  97. break;
  98. case '/plaintext':
  99. _plaintextTest(request);
  100. break;
  101. default:
  102. _sendResponse(request, HttpStatus.NOT_FOUND);
  103. break;
  104. }
  105. });
  106. });
  107. }
  108. /// Returns the given [text] parsed as a base 10 integer. If the text is null
  109. /// or is an otherwise invalid representation of a base 10 integer, zero is
  110. /// returned.
  111. int _parseInt(String text) =>
  112. (text == null) ? 0 : int.parse(text, radix: 10, onError: ((_) => 0));
  113. /// Completes the given [request] by writing the [response] with the given
  114. /// [statusCode] and [type].
  115. void _sendResponse(HttpRequest request, int statusCode, [type, response]) {
  116. request.response.statusCode = statusCode;
  117. request.response.headers.date = new DateTime.now();
  118. if (type != null) {
  119. request.response.headers.contentType = type;
  120. }
  121. if (response != null) {
  122. var data = UTF8.encode(response);
  123. request.response.contentLength = data.length;
  124. request.response.add(data);
  125. } else {
  126. request.response.contentLength = 0;
  127. }
  128. request.response.close();
  129. }
  130. /// Completes the given [request] by writing the [response] as HTML.
  131. void _sendHtml(HttpRequest request, String response) {
  132. _sendResponse(request, HttpStatus.OK, ContentType.HTML, response);
  133. }
  134. /// Completes the given [request] by writing the [response] as JSON.
  135. void _sendJson(HttpRequest request, Object response) {
  136. _sendResponse(
  137. request, HttpStatus.OK, ContentType.JSON, JSON.encode(response));
  138. }
  139. /// Completes the given [request] by writing the [response] as plain text.
  140. void _sendText(HttpRequest request, String response) {
  141. _sendResponse(request, HttpStatus.OK, ContentType.TEXT, response);
  142. }
  143. /// Responds with the JSON test to the [request].
  144. void _jsonTest(HttpRequest request) {
  145. _sendJson(request, {'message': 'Hello, World!'});
  146. }
  147. Future<World> _queryRandom() {
  148. return _connectionPool.connect().then((connection) {
  149. return connection.query(
  150. 'SELECT id, randomnumber FROM world WHERE id = @id;', {
  151. 'id': _RANDOM.nextInt(_WORLD_TABLE_SIZE) + 1
  152. })
  153. //
  154. // The benchmark's constraints tell us there is exactly one row.
  155. //
  156. .single.then((row) => new World(row[0], row[1])).whenComplete(() {
  157. connection.close();
  158. });
  159. });
  160. }
  161. /// Responds with the database query test to the [request].
  162. void _dbTest(HttpRequest request) {
  163. _queryRandom().then((response) => _sendJson(request, response));
  164. }
  165. /// Responds with the database queries test to the [request].
  166. void _queriesTest(HttpRequest request) {
  167. var queries = _parseInt(request.uri.queryParameters['queries']).clamp(1, 500);
  168. Future
  169. .wait(new List.generate(queries, (_) => _queryRandom(), growable: false))
  170. .then((response) => _sendJson(request, response));
  171. }
  172. /// Responds with the fortunes test to the [request].
  173. void _fortunesTest(HttpRequest request) {
  174. _connectionPool.connect().then((connection) {
  175. return connection
  176. .query('SELECT id, message FROM fortune;')
  177. .map((row) => new Fortune(row[0], row[1]))
  178. .toList()
  179. .whenComplete(() {
  180. connection.close();
  181. });
  182. }).then((fortunes) {
  183. fortunes.add(new Fortune(0, 'Additional fortune added at request time.'));
  184. fortunes.sort();
  185. _sendHtml(request, _fortunesTemplate.renderString({
  186. 'fortunes': fortunes
  187. .map((fortune) => {'id': fortune.id, 'message': fortune.message})
  188. .toList()
  189. }));
  190. });
  191. }
  192. /// Responds with the updates test to the [request].
  193. void _updatesTest(HttpRequest request) {
  194. var queries = _parseInt(request.uri.queryParameters['queries']).clamp(1, 500);
  195. Future.wait(new List.generate(queries, (_) {
  196. return _queryRandom().then((world) {
  197. world.randomnumber = _RANDOM.nextInt(_WORLD_TABLE_SIZE) + 1;
  198. return _connectionPool.connect().then((connection) {
  199. return connection
  200. .execute(
  201. 'UPDATE world SET randomnumber = @randomnumber WHERE id = @id;',
  202. {'randomnumber': world.randomnumber, 'id': world.id})
  203. .whenComplete(() {
  204. connection.close();
  205. });
  206. }).then((_) => world);
  207. });
  208. }, growable: false)).then((worlds) => _sendJson(request, worlds));
  209. }
  210. /// Responds with the plaintext test to the [request].
  211. void _plaintextTest(HttpRequest request) {
  212. _sendText(request, 'Hello, World!');
  213. }