server.dart 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import 'dart:async' show Future;
  2. import 'dart:io';
  3. import 'dart:json' as json;
  4. import 'dart:math' show Random;
  5. import 'dart:utf' as utf;
  6. import 'package:args/args.dart' show ArgParser, ArgResults;
  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. /**
  12. * Starts a new HTTP server that implements the tests to be benchmarked. The
  13. * address and port for incoming connections is configurable via command line
  14. * arguments, as is the number of database connections to be maintained in the
  15. * connection pool.
  16. */
  17. main() {
  18. ArgParser parser = new ArgParser();
  19. parser.addOption('address', abbr: 'a', defaultsTo: '0.0.0.0');
  20. parser.addOption('port', abbr: 'p', defaultsTo: '8080');
  21. parser.addOption('dbconnections', abbr: 'd', defaultsTo: '256');
  22. ArgResults arguments = parser.parse(new Options().arguments);
  23. _startServer(
  24. arguments['address'],
  25. int.parse(arguments['port']),
  26. int.parse(arguments['dbconnections']));
  27. }
  28. /**
  29. * The entity used in the database query and update tests.
  30. */
  31. class World {
  32. int id;
  33. int randomNumber;
  34. World(this.id, this.randomNumber);
  35. toJson() => { 'id': id, 'randomNumber': randomNumber };
  36. }
  37. /**
  38. * The entity used in the fortunes test.
  39. */
  40. class Fortune implements Comparable<Fortune> {
  41. int id;
  42. String message;
  43. Fortune(this.id, this.message);
  44. int compareTo(Fortune other) => message.compareTo(other.message);
  45. }
  46. /**
  47. * The number of rows in the world entity table.
  48. */
  49. const _WORLD_TABLE_SIZE = 10000;
  50. /**
  51. * A random number generator.
  52. */
  53. final _RANDOM = new Random();
  54. /**
  55. * The 'text/html; charset=utf-8' content type.
  56. */
  57. final _TYPE_HTML = new ContentType('text', 'html', charset: 'utf-8');
  58. /**
  59. * The 'application/json; charset=utf-8' content type.
  60. */
  61. final _TYPE_JSON = new ContentType('application', 'json', charset: 'utf-8');
  62. /**
  63. * The PostgreSQL connection pool used by all the tests that require database
  64. * connectivity.
  65. */
  66. pgpool.Pool _connectionPool;
  67. /**
  68. * The mustache template which is rendered in the fortunes test.
  69. */
  70. mustache.Template _fortunesTemplate;
  71. /**
  72. * Starts a benchmark server, which listens for connections from
  73. * '[address] : [port]' and maintains [dbConnections] connections to the
  74. * database.
  75. */
  76. void _startServer(String address, int port, int dbConnections) {
  77. Future.wait([
  78. new File('postgresql.yaml').readAsString().then((String config) {
  79. _connectionPool = new pgpool.Pool(
  80. new pg.Settings.fromMap(yaml.loadYaml(config)).toUri(),
  81. min: dbConnections,
  82. max: dbConnections);
  83. return _connectionPool.start();
  84. }),
  85. new File('fortunes.mustache').readAsString().then((String template) {
  86. _fortunesTemplate = mustache.parse(template);
  87. })
  88. ]).then((_) {
  89. HttpServer.bind(address, port).then((HttpServer server) {
  90. server.listen((HttpRequest request) {
  91. switch (request.uri.path) {
  92. case '/': return _jsonTest(request);
  93. case '/db': return _dbTest(request);
  94. case '/fortunes': return _fortunesTest(request);
  95. case '/update': return _updateTest(request);
  96. default: return _sendResponse(request, HttpStatus.NOT_FOUND);
  97. }
  98. });
  99. });
  100. });
  101. }
  102. /**
  103. * Returns the given [text] parsed as a base 10 integer. If the text is null
  104. * or is an otherwise invalid representation of a base 10 integer, zero is
  105. * returned.
  106. */
  107. int _parseInt(String text) =>
  108. (text == null) ? 0 : int.parse(text, radix: 10, onError: ((_) => 0));
  109. /**
  110. * Completes the given [request] by writing the [response] with the given
  111. * [statusCode] and [type].
  112. */
  113. void _sendResponse(HttpRequest request, int statusCode,
  114. [ ContentType type, String response ]) {
  115. request.response.statusCode = statusCode;
  116. request.response.headers.add(
  117. HttpHeaders.CONNECTION,
  118. (request.persistentConnection) ? 'keep-alive' : 'close');
  119. request.response.headers.add(HttpHeaders.SERVER, 'dart');
  120. request.response.headers.date = new DateTime.now();
  121. if (type != null) {
  122. request.response.headers.contentType = type;
  123. }
  124. if (response != null) {
  125. //
  126. // A simpler way to write a response would be to:
  127. //
  128. // 1. Not set the contentLength header.
  129. // 2. Use response.write instead of response.add.
  130. //
  131. // However, doing that results in a chunked, gzip-encoded response, and
  132. // gzip is explicitly disallowed by the requirements for these benchmark
  133. // tests.
  134. //
  135. // See: http://www.techempower.com/benchmarks/#section=code
  136. //
  137. List<int> encodedResponse = utf.encodeUtf8(response);
  138. request.response.headers.contentLength = encodedResponse.length;
  139. request.response.add(encodedResponse);
  140. }
  141. //
  142. // The load-testing tool will close any currently open connection at the end
  143. // of each run. That potentially causes an error to be thrown here. Since
  144. // we want the server to remain alive for subsequent runs, we catch the
  145. // error.
  146. //
  147. request.response.close().catchError(print);
  148. }
  149. /**
  150. * Completes the given [request] by writing the [response] as HTML.
  151. */
  152. void _sendHtml(HttpRequest request, String response) {
  153. _sendResponse(request, HttpStatus.OK, _TYPE_HTML, response);
  154. }
  155. /**
  156. * Completes the given [request] by writing the [response] as JSON.
  157. */
  158. void _sendJson(HttpRequest request, Object response) {
  159. _sendResponse(request, HttpStatus.OK, _TYPE_JSON, json.stringify(response));
  160. }
  161. /**
  162. * Responds with the JSON test to the [request].
  163. */
  164. void _jsonTest(HttpRequest request) {
  165. _sendJson(request, { 'message': 'Hello, World!' });
  166. }
  167. /**
  168. * Responds with the database query test to the [request].
  169. */
  170. void _dbTest(HttpRequest request) {
  171. int queries = _parseInt(request.queryParameters['queries']).clamp(1, 500);
  172. List<World> worlds = new List<World>(queries);
  173. Future.wait(new List.generate(queries, (int index) {
  174. return _connectionPool.connect().then((pg.Connection connection) {
  175. return connection.query(
  176. 'SELECT id, randomNumber FROM world WHERE id = @id;',
  177. { 'id': _RANDOM.nextInt(_WORLD_TABLE_SIZE) + 1 })
  178. .map((row) => new World(row[0], row[1]))
  179. //
  180. // The benchmark's constraints tell us there is exactly one row here.
  181. //
  182. .forEach((World world) { worlds[index] = world; })
  183. .then((_) { connection.close(); });
  184. });
  185. }, growable: false)).then((_) { _sendJson(request, worlds); });
  186. }
  187. /**
  188. * Responds with the fortunes test to the [request].
  189. */
  190. void _fortunesTest(HttpRequest request) {
  191. List<Fortune> fortunes = [];
  192. _connectionPool.connect().then((pg.Connection connection) {
  193. return connection.query('SELECT id, message FROM fortune;')
  194. .map((row) => new Fortune(row[0], row[1]))
  195. .forEach(fortunes.add)
  196. .then((_) { connection.close(); });
  197. }).then((_) {
  198. fortunes.add(new Fortune(0, 'Additional fortune added at request time.'));
  199. fortunes.sort();
  200. _sendHtml(request, _fortunesTemplate.renderString({
  201. 'fortunes': fortunes.map((Fortune fortune) => {
  202. 'id': fortune.id, 'message': fortune.message
  203. }).toList()
  204. }));
  205. });
  206. }
  207. /**
  208. * Responds with the updates test to the [request].
  209. */
  210. void _updateTest(HttpRequest request) {
  211. int queries = _parseInt(request.queryParameters['queries']).clamp(1, 500);
  212. List<World> worlds = new List<World>(queries);
  213. Future.wait(new List.generate(queries, (int index) {
  214. return _connectionPool.connect().then((pg.Connection connection) {
  215. return connection.query(
  216. 'SELECT id, randomNumber FROM world WHERE id = @id;',
  217. { 'id': _RANDOM.nextInt(_WORLD_TABLE_SIZE) + 1 })
  218. .map((row) => new World(row[0], row[1]))
  219. //
  220. // The benchmark's constraints tell us there is exactly one row here.
  221. //
  222. .forEach((World world) { worlds[index] = world; })
  223. .then((_) { connection.close(); });
  224. });
  225. }, growable: false)).then((_) {
  226. Future.wait(new List.generate(queries, (int index) {
  227. World world = worlds[index];
  228. world.randomNumber = _RANDOM.nextInt(_WORLD_TABLE_SIZE) + 1;
  229. return _connectionPool.connect().then((pg.Connection connection) {
  230. return connection.execute(
  231. 'UPDATE world SET randomNumber = @randomNumber WHERE id = @id;',
  232. { 'randomNumber': world.randomNumber, 'id': world.id })
  233. .then((_) { connection.close(); });
  234. });
  235. }, growable: false)).then((_) { _sendJson(request, worlds); });
  236. });
  237. }