server.dart 7.6 KB

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