server.dart 7.4 KB

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