Skip to content

Commit dad4df9

Browse files
authored
Add /data/environment route (#961)
This new `/data/environment` route (of the core plugin) responds with base properties relevant to the frontend upon startup of TensorBoard. These properties include: * the window title * the location of data - either a path to a log directory or a database URI. This new route will enable the frontend to display the data location on the bottom left when TensorBoard runs in database mode (once the frontend makes use of this backend change in a different PR): ![image](https://user-images.githubusercontent.com/4221553/35950853-7c5f8a44-0c2d-11e8-97d8-49024d9a640c.png)
1 parent fa2e329 commit dad4df9

File tree

7 files changed

+110
-23
lines changed

7 files changed

+110
-23
lines changed

tensorboard/backend/application.py

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def standard_tensorboard_wsgi(
118118
context = base_plugin.TBContext(
119119
db_module=db_module,
120120
db_connection_provider=db_connection_provider,
121+
db_uri=db_uri,
121122
logdir=logdir,
122123
multiplexer=multiplexer,
123124
assets_zip_provider=assets_zip_provider,

tensorboard/backend/application_test.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,9 @@ def setUp(self):
357357
]
358358

359359
# The application should have added routes for both plugins.
360-
self.app = application.standard_tensorboard_wsgi('', True, 60, plugins)
360+
self.logdir = self.get_temp_dir()
361+
self.app = application.standard_tensorboard_wsgi(
362+
self.logdir, True, 60, plugins)
361363

362364
def _foo_handler(self):
363365
pass

tensorboard/plugins/base_plugin.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __init__(
8686
assets_zip_provider=None,
8787
db_connection_provider=None,
8888
db_module=None,
89+
db_uri=None,
8990
logdir=None,
9091
multiplexer=None,
9192
plugin_name_to_instance=None,
@@ -112,7 +113,10 @@ def __init__(
112113
db_module: A PEP-249 DB Module, e.g. sqlite3. This is useful for accessing
113114
things like date time constructors. This value will be None if we are
114115
not in SQL mode and multiplexer should be used instead.
115-
logdir: The string logging directory TensorBoard was started with.
116+
db_uri: The string db URI TensorBoard was started with. If this is set,
117+
the logdir should be None.
118+
logdir: The string logging directory TensorBoard was started with. If this
119+
is set, the db_uri should be None.
116120
multiplexer: An EventMultiplexer with underlying TB data. Plugins should
117121
copy this data over to the database when the db fields are set.
118122
plugin_name_to_instance: A mapping between plugin name to instance.
@@ -126,6 +130,7 @@ def __init__(
126130
self.assets_zip_provider = assets_zip_provider
127131
self.db_connection_provider = db_connection_provider
128132
self.db_module = db_module
133+
self.db_uri = db_uri
129134
self.logdir = logdir
130135
self.multiplexer = multiplexer
131136
self.plugin_name_to_instance = plugin_name_to_instance

tensorboard/plugins/core/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ py_test(
3232
srcs_version = "PY2AND3",
3333
deps = [
3434
":core_plugin",
35+
"//tensorboard:db",
3536
"//tensorboard:expect_tensorflow_installed",
3637
"//tensorboard/backend:application",
3738
"//tensorboard/backend/event_processing:event_multiplexer",

tensorboard/plugins/core/core_plugin.py

+25
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ def __init__(self, context):
4848
context: A base_plugin.TBContext instance.
4949
"""
5050
self._logdir = context.logdir
51+
self._db_uri = context.db_uri
5152
self._window_title = context.window_title
5253
self._multiplexer = context.multiplexer
54+
self._db_connection_provider = context.db_connection_provider
5355
self._assets_zip_provider = context.assets_zip_provider
5456

5557
def is_active(self):
@@ -59,6 +61,7 @@ def get_plugin_apps(self):
5961
apps = {
6062
'/___rPc_sWiTcH___': self._send_404_without_logging,
6163
'/audio': self._redirect_to_index,
64+
'/data/environment': self._serve_environment,
6265
'/data/logdir': self._serve_logdir,
6366
'/data/runs': self._serve_runs,
6467
'/data/window_properties': self._serve_window_properties,
@@ -93,15 +96,36 @@ def _serve_asset(self, path, gzipped_asset_bytes, request):
9396
return http_util.Respond(
9497
request, gzipped_asset_bytes, mimetype, content_encoding='gzip')
9598

99+
@wrappers.Request.application
100+
def _serve_environment(self, request):
101+
"""Serve a JSON object containing some base properties used by the frontend.
102+
103+
* data_location is either a path to a directory or an address to a
104+
database (depending on which mode TensorBoard is running in).
105+
* window_title is the title of the TensorBoard web page.
106+
"""
107+
return http_util.Respond(
108+
request,
109+
{
110+
'data_location': self._logdir or self._db_uri,
111+
'window_title': self._window_title,
112+
},
113+
'application/json')
114+
96115
@wrappers.Request.application
97116
def _serve_logdir(self, request):
98117
"""Respond with a JSON object containing this TensorBoard's logdir."""
118+
# TODO(chihuahua): Remove this method once the frontend instead uses the
119+
# /data/environment route (and no deps throughout Google use the
120+
# /data/logdir route).
99121
return http_util.Respond(
100122
request, {'logdir': self._logdir}, 'application/json')
101123

102124
@wrappers.Request.application
103125
def _serve_window_properties(self, request):
104126
"""Serve a JSON object containing this TensorBoard's window properties."""
127+
# TODO(chihuahua): Remove this method once the frontend instead uses the
128+
# /data/environment route.
105129
return http_util.Respond(
106130
request, {'window_title': self._window_title}, 'application/json')
107131

@@ -118,6 +142,7 @@ def _serve_runs(self, request):
118142
A werkzeug Response with the following content:
119143
{runName: {firstEventTimestamp: 123456.789}}
120144
"""
145+
# TODO(chihuahua): When running in database mode, query the Runs table.
121146
run_names = sorted(self._multiplexer.Runs()) # Why `sorted`? See below.
122147
def get_first_event_timestamp(run_name):
123148
try:

tensorboard/plugins/core/core_plugin_test.py

+70-17
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from __future__ import print_function
2020

2121
import collections
22+
import contextlib
2223
import json
2324
import os
2425
import shutil
@@ -27,6 +28,7 @@
2728
from werkzeug import test as werkzeug_test
2829
from werkzeug import wrappers
2930

31+
from tensorboard import db
3032
from tensorboard.backend import application
3133
from tensorboard.backend.event_processing import plugin_event_multiplexer as event_multiplexer # pylint: disable=line-too-long
3234
from tensorboard.plugins import base_plugin
@@ -37,30 +39,60 @@ class CorePluginTest(tf.test.TestCase):
3739
_only_use_meta_graph = False # Server data contains only a GraphDef
3840

3941
def setUp(self):
40-
self.logdir = self.get_temp_dir()
41-
self.addCleanup(shutil.rmtree, self.logdir)
42+
self.temp_dir = self.get_temp_dir()
43+
self.addCleanup(shutil.rmtree, self.temp_dir)
44+
45+
self.startLogdirBasedServer(self.temp_dir)
46+
self.startDbBasedServer(self.temp_dir)
47+
48+
def startLogdirBasedServer(self, temp_dir):
49+
self.logdir = temp_dir
4250
self._generate_test_data(run_name='run1')
4351
self.multiplexer = event_multiplexer.EventMultiplexer(
4452
size_guidance=application.DEFAULT_SIZE_GUIDANCE,
4553
purge_orphaned_data=True)
46-
self._context = base_plugin.TBContext(
54+
context = base_plugin.TBContext(
4755
assets_zip_provider=get_test_assets_zip_provider(),
4856
logdir=self.logdir,
49-
multiplexer=self.multiplexer)
50-
self.plugin = core_plugin.CorePlugin(self._context)
57+
multiplexer=self.multiplexer,
58+
window_title='title foo')
59+
self.logdir_based_plugin = core_plugin.CorePlugin(context)
5160
app = application.TensorBoardWSGIApp(
52-
self.logdir, [self.plugin], self.multiplexer, 0, path_prefix='')
53-
self.server = werkzeug_test.Client(app, wrappers.BaseResponse)
61+
self.logdir,
62+
[self.logdir_based_plugin],
63+
self.multiplexer,
64+
0,
65+
path_prefix='')
66+
self.logdir_based_server = werkzeug_test.Client(app, wrappers.BaseResponse)
67+
68+
def startDbBasedServer(self, temp_dir):
69+
self.db_uri = 'sqlite:' + os.path.join(temp_dir, 'db.sqlite')
70+
db_module, db_connection_provider = application.get_database_info(
71+
self.db_uri)
72+
if db_connection_provider is not None:
73+
with contextlib.closing(db_connection_provider()) as db_conn:
74+
schema = db.Schema(db_conn)
75+
schema.create_tables()
76+
schema.create_indexes()
77+
context = base_plugin.TBContext(
78+
assets_zip_provider=get_test_assets_zip_provider(),
79+
db_module=db_module,
80+
db_connection_provider=db_connection_provider,
81+
db_uri=self.db_uri,
82+
window_title='title foo')
83+
self.db_based_plugin = core_plugin.CorePlugin(context)
84+
app = application.TensorBoardWSGI([self.db_based_plugin])
85+
self.db_based_server = werkzeug_test.Client(app, wrappers.BaseResponse)
5486

5587
def testRoutesProvided(self):
5688
"""Tests that the plugin offers the correct routes."""
57-
routes = self.plugin.get_plugin_apps()
89+
routes = self.logdir_based_plugin.get_plugin_apps()
5890
self.assertIsInstance(routes['/data/logdir'], collections.Callable)
5991
self.assertIsInstance(routes['/data/runs'], collections.Callable)
6092

6193
def testIndex_returnsActualHtml(self):
6294
"""Test the format of the /data/runs endpoint."""
63-
response = self.server.get('/')
95+
response = self.logdir_based_server.get('/')
6496
self.assertEqual(200, response.status_code)
6597
self.assertStartsWith(response.headers.get('Content-Type'), 'text/html')
6698
html = response.get_data()
@@ -69,18 +101,39 @@ def testIndex_returnsActualHtml(self):
69101
def testDataPaths_disableAllCaching(self):
70102
"""Test the format of the /data/runs endpoint."""
71103
for path in ('/data/runs', '/data/logdir'):
72-
response = self.server.get(path)
104+
response = self.logdir_based_server.get(path)
73105
self.assertEqual(200, response.status_code, msg=path)
74106
self.assertEqual('0', response.headers.get('Expires'), msg=path)
75107

108+
def testEnvironmentForDbUri(self):
109+
"""Test that the environment route correctly returns the database URI."""
110+
parsed_object = self._get_json(self.db_based_server, '/data/environment')
111+
self.assertEqual(parsed_object['data_location'], self.db_uri)
112+
113+
def testEnvironmentForLogdir(self):
114+
"""Test that the environment route correctly returns the logdir."""
115+
parsed_object = self._get_json(
116+
self.logdir_based_server, '/data/environment')
117+
self.assertEqual(parsed_object['data_location'], self.logdir)
118+
119+
def testEnvironmentForWindowTitle(self):
120+
"""Test that the environment route correctly returns the window title."""
121+
parsed_object_db = self._get_json(
122+
self.db_based_server, '/data/environment')
123+
parsed_object_logdir = self._get_json(
124+
self.logdir_based_server, '/data/environment')
125+
self.assertEqual(
126+
parsed_object_db['window_title'], parsed_object_logdir['window_title'])
127+
self.assertEqual(parsed_object_db['window_title'], 'title foo')
128+
76129
def testLogdir(self):
77130
"""Test the format of the data/logdir endpoint."""
78-
parsed_object = self._get_json('/data/logdir')
131+
parsed_object = self._get_json(self.logdir_based_server, '/data/logdir')
79132
self.assertEqual(parsed_object, {'logdir': self.logdir})
80133

81134
def testRuns(self):
82135
"""Test the format of the /data/runs endpoint."""
83-
run_json = self._get_json('/data/runs')
136+
run_json = self._get_json(self.logdir_based_server, '/data/runs')
84137
self.assertEqual(run_json, ['run1'])
85138

86139
def testRunsAppendOnly(self):
@@ -120,23 +173,23 @@ def add_run(run_name):
120173

121174
# Add one run: it should come last.
122175
add_run('avocado')
123-
self.assertEqual(self._get_json('/data/runs'),
176+
self.assertEqual(self._get_json(self.logdir_based_server, '/data/runs'),
124177
['run1', 'avocado'])
125178

126179
# Add another run: it should come last, too.
127180
add_run('zebra')
128-
self.assertEqual(self._get_json('/data/runs'),
181+
self.assertEqual(self._get_json(self.logdir_based_server, '/data/runs'),
129182
['run1', 'avocado', 'zebra'])
130183

131184
# And maybe there's a run for which we somehow have no timestamp.
132185
add_run('mysterious')
133-
self.assertEqual(self._get_json('/data/runs'),
186+
self.assertEqual(self._get_json(self.logdir_based_server, '/data/runs'),
134187
['run1', 'avocado', 'zebra', 'mysterious'])
135188

136189
stubs.UnsetAll()
137190

138-
def _get_json(self, path):
139-
response = self.server.get(path)
191+
def _get_json(self, server, path):
192+
response = server.get(path)
140193
self.assertEqual(200, response.status_code)
141194
return self._get_json_payload(response)
142195

tensorboard/plugins/text/text_plugin_test.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -358,25 +358,25 @@ def assertIsActive(self, plugin, expected_is_active):
358358
def testPluginIsActiveWhenNoRuns(self):
359359
"""The plugin should be inactive when there are no runs."""
360360
multiplexer = event_multiplexer.EventMultiplexer()
361-
context = base_plugin.TBContext(logdir=None, multiplexer=multiplexer)
361+
context = base_plugin.TBContext(logdir=self.logdir, multiplexer=multiplexer)
362362
plugin = text_plugin.TextPlugin(context)
363363
self.assertIsActive(plugin, False)
364364

365365
def testPluginIsActiveWhenTextRuns(self):
366366
"""The plugin should be active when there are runs with text."""
367367
multiplexer = event_multiplexer.EventMultiplexer()
368-
context = base_plugin.TBContext(logdir=None, multiplexer=multiplexer)
368+
context = base_plugin.TBContext(logdir=self.logdir, multiplexer=multiplexer)
369369
plugin = text_plugin.TextPlugin(context)
370370
multiplexer.AddRunsFromDirectory(self.logdir)
371371
multiplexer.Reload()
372372
self.assertIsActive(plugin, True)
373373

374374
def testPluginIsActiveWhenRunsButNoText(self):
375375
"""The plugin should be inactive when there are runs but none has text."""
376+
logdir = os.path.join(self.get_temp_dir(), 'runs_with_no_text')
376377
multiplexer = event_multiplexer.EventMultiplexer()
377-
context = base_plugin.TBContext(logdir=None, multiplexer=multiplexer)
378+
context = base_plugin.TBContext(logdir=logdir, multiplexer=multiplexer)
378379
plugin = text_plugin.TextPlugin(context)
379-
logdir = os.path.join(self.get_temp_dir(), 'runs_with_no_text')
380380
self.generate_testdata(include_text=False, logdir=logdir)
381381
multiplexer.AddRunsFromDirectory(logdir)
382382
multiplexer.Reload()

0 commit comments

Comments
 (0)