Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes path_prefix flag #1623

Merged
merged 10 commits into from
Nov 29, 2018
Merged

Fixes path_prefix flag #1623

merged 10 commits into from
Nov 29, 2018

Conversation

stephanwlee
Copy link
Contributor

PR #1512 introduced a bug where it formed a complete path without using
existing pathname. Previously without forming the complete path, it was
treated as a relative path from current window.location.pathname.

@@ -62,7 +62,13 @@ export function createRouter(pathPrefix = 'data', demoMode = false): Router {
};
};

let _router: Router = createRouter();
export function getDefaultRouter(): Router {
const sep = window.location.pathname.endsWith('/') ? '' : '/';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not great that this introduces a side-effect at module load time.
We now need to reason about the first time that this module is imported;
it becomes harder to reason about in isolation.

Before #1512, we issued requests to relative paths. Can we just go back
to doing that instead of switching to absolute paths and then trying to
relativize them? We wouldn’t need to worry about window.location at
all, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I contemplated and decided to keep the logic that uses new URL for its sanitization/normalization but unfortunately forces us to reason with absolute path. Not sure if I understand what you mean by side-effect at the bootstrap time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I haven’t carefully looked at the ways in which we use URL;
I’ll do so after the holiday.

* @param demoMode {boolean=} Whether to modify urls for filesystem demo usage.
*/
export function createRouter(pathPrefix = 'data', demoMode = false): Router {
export function createRouter(pathPrefix, demoMode = false): Router {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the default prefix no longer "data"? This is a breaking
change—does it just not happen to break any clients?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vetted all the callsites manually

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vetted all the callsites manually

I mean, this is not possible in principle, right? createRouter is a
publicly exported function defined in a module whose Bazel target has
visibility //visibility:public in a workspace that we explicitly
encourage people to import, both inside Google and beyond. By all
accounts it is a public API. Are we in the habit of breaking our public
contracts?

I also still don’t understand why you want to make this change.

@@ -81,6 +81,23 @@ describe('backend tests', () => {
assert.equal(r.runs(), '/foo/runs');
});

describe('default router', () => {
beforeEach(function() {
this.router = getDefaultRouter();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why assign to this.router and introduce this state instead of just
using

        assert.equal(
            getDefaultRouter().runs(),
            `${window.location.pathname}/data/runs`
        );

in the one test case that actually uses this?

PR tensorflow#1512 introduced a bug where it formed a complete path without using
existing pathname. Previously without forming the complete path, it was
treated as a relative path from current window.location.pathname.
@stephanwlee
Copy link
Contributor Author

Argh, I just read the proper API doc for URL and found out the constructor takes an optional second parameter letting us achieve what we wanted. I have revised the code. PTAL

Copy link
Contributor

@wchargin wchargin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely better: thanks. It’s unfortunate that we depend on
window.location at all, but I see that existing code does this, too,
so at least this isn’t a regression.

});

it('ignores custom extension', function() {
assert.equal(
assertRelativePath(
this.router.pluginRoute('scalars', '/a', undefined, 'meow'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this typecheck? The Router interface declares pluginRoute
as a function taking two parameters, not four. Do we have implicit any
propagation somehow?

Copy link
Contributor Author

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that’s not what I’m concerned about. That’s perfectly fine.

At the callsite in this comment, you are invoking a function with more
arguments than the number of declared parameters. TypeScript disallows
this
(which it must—this is the only sound thing to do). But this
type error was not caught. Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a brief experiment, it looks like attaching router onto Mocha's scope makes us lose all the typing info. Will refactor to avoid that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that’s what I suspected. Thanks: please do refactor thus.

// Use URL to normalize pathPrefix with leading slash and without.
url.pathname = pathPrefix + path;
const maybeRelativePath = pathPrefix + path;
const url = new URL(maybeRelativePath, window.location.href);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to use the actual window.location here? Could we just pass "http://localhost" or even "http://ignored" instead? That would make it clearer that we're only dealing with relative URL paths.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of using window.location.href is that if you’ve navigated to
http://localhost:6006/some-prefix/ then that prefix will be included
in the eventual URL. Doesn’t this break if you use "http://localhost"
instead of window.location.href? Am I missing something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But maybeRelativePath already includes the configured pathPrefix. Unless the idea here was to make TensorBoard automatically detect the pathPrefix from the href, I don't think it's necessary to get it from window.location.href.

And actually, I don't think any path information is retained from the second parameter to URL at all, from testing it just now - it only extracts the origin anyway:

> new URL('prefix/foo', 'http://xyz')
URL {href: "http://xyz/prefix/foo", origin: "http://xyz", protocol: "http:", username: "", password: "", …}
> new URL('prefix/foo', 'http://xyz/')
URL {href: "http://xyz/prefix/foo", origin: "http://xyz", protocol: "http:", username: "", password: "", …}
> new URL('prefix/foo', 'http://xyz/foo')
URL {href: "http://xyz/prefix/foo", origin: "http://xyz", protocol: "http:", username: "", password: "", …}
> new URL('/prefix/foo', 'http://xyz/foo')
URL {href: "http://xyz/prefix/foo", origin: "http://xyz", protocol: "http:", username: "", password: "", …}

Copy link
Contributor Author

@stephanwlee stephanwlee Nov 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

> new URL('prefix/foo', 'http://xyz/foo/bar')
< URL {href: "http://xyz/foo/prefix/foo", origin: "http://xyz", protocol: "http:", username: "", password: "", …}
> new URL('prefix/foo', 'http://xyz/foo/')
< URL {href: "http://xyz/foo/prefix/foo", origin: "http://xyz", protocol: "http:", username: "", password: "", …}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your tests are correct but nonexhaustive. If you’re on http://xyz/foo
and navigate to prefix/bar, you will visit http://xyz/prefix/bar.
You need to include a trailing slash to force an additional path
component:

> String(new URL("y/z", "http://foo/"))
"http://foo/y/z"
> String(new URL("y/z", "http://foo/bar/"))
"http://foo/bar/y/z"
> String(new URL("y/z", "http://foo/bar/baz/"))
"http://foo/bar/baz/y/z"

But maybeRelativePath already includes the configured pathPrefix

Yes, but the pathPrefix is always "data", right? (Admittedly, this
naming is very misleading.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, you're right. It also turns out the pathPrefix naming situation was exacerbated when I suggested renaming dataDir to pathPrefix because I had managed to conflate dataDir and the --path_prefix flag:
#1563 (comment)

So really we should probably change everything here back to dataDir instead of pathPrefix. The router used to entirely ignore the path prefix (because it only operated on relative paths). With this change, the use of window.location.href will bake it into the final pathname as an absolute URL path (starting with /), but it's still not explicitly referenced.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The router used to entirely ignore the path prefix (because it only
operated on relative paths). With this change, the use of
window.location.href will bake it into the final pathname as an
absolute URL path (starting with /), but it's still not explicitly
referenced.

Yes, exactly.

So really we should probably change everything here back to dataDir
instead of pathPrefix.

Fine with me. It’s worth noting that the value of this argument is in
fact always "data" in public code, except for tests of the router
itself.

Copy link
Contributor

@wchargin wchargin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks better; thanks!

Some questions since I’ve never actually seen these demos working:

  • What’s our procedure for testing the demo logic?
  • How do we generate the data for these demos, and how do we run them?
  • Can we run them with a --path_prefix, or are path prefixes and demo mode mutually exclusive?
  • Is any of this automated?

url.pathname = `${pathPrefix}/${normalizedPath}${ext}`;
return url.pathname + url.search;
url.pathname = `${dataDir}/${normalizedPath}${ext}`;
return url.pathname;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be safely replaced with

return `${dataDir}/${normalizedPath}${ext}`;

or do we need the special getter-setter semantics of URL.pathname?
(For instance, the current code will always return a string that starts
with a slash, even if dataDir does not. Is this intentional?)

If the latter, could you please leave a comment explaining why? It’s
certainly not obvious at a glance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For instance, the current code will always return a string that starts
with a slash, even if dataDir does not. Is this intentional?

Yup, this is intentional. demos data shims are served at /data/ regardless of --path_prefix so we want to form an absolute path.
About using getter-setter, I was implicitly using URL's ability to sanitize the path and form a legal URL. Because we had undone that part in prodURL, technically, there is no reason to do that for demo url. (moreover, because we do not render output of the router into source of an anchor tag or in dangerous way, I think the sanitization of URL we form is not strictly required so that's that)

If the latter, could you please leave a comment explaining why
Will do

if (params) url.search = params.toString();
return url.pathname + url.search;
let relativePath = dataDir + route;
if (params) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note (not a big deal): if I read this correctly, the behavior is
different when params is new URLSearchParams() vs. undefined: in
the former case you get URLs like /data/foo? and /data/foo?bar=1&,
while in the latter case you get /data/foo and /data/foo?bar=1. To
my knowledge, these should be handled identically by the backend, so I’m
fine with the code as is, even though it would IMHO be more sensible to
write as

function createProdDataPath(
  dataDir: string,
  route: string,
  ext: string,
  params: URLSearchParams = new URLSearchParams()
): string {
  let relativePath = dataDir + route;
  if (String(params)) {
    const delimiter = route.includes("?") ? "&" : "?";
    relativePath += delimiter + String(params);
  }
  return relativePath;
}

@wchargin
Copy link
Contributor

Thanks! I haven’t read the demo-mode code/tests very carefully, as we’re
just going to remove those (#1640). Prod code looks good to me.

@stephanwlee stephanwlee merged commit 71f68bf into tensorflow:master Nov 29, 2018
@stephanwlee stephanwlee deleted the fix3 branch November 29, 2018 19:10
@falaki
Copy link

falaki commented Dec 27, 2018

Hi @stephanwlee and @wchargin is this bug fix going to be included in a patch release of 1.12 (e.g. 1.12.2)?

@stephanwlee
Copy link
Contributor Author

@falaki Sorry, this change did not make it into 1.12.1 and we do not have a plan to release another patch before 1.13. However, 1.13 should come out early next year.

stephanwlee added a commit to stephanwlee/tensorboard that referenced this pull request Jan 2, 2019
PR tensorflow#1512 introduced a bug where it formed a complete path without using
existing pathname. Previously without forming the complete path, it was
treated as a relative path from current window.location.pathname. We decided
to revert back to the old behavior without using `new URL` which was not
providing a clear benefit.
stephanwlee added a commit that referenced this pull request Jan 3, 2019
PR #1512 introduced a bug where it formed a complete path without using
existing pathname. Previously without forming the complete path, it was
treated as a relative path from current window.location.pathname. We decided
to revert back to the old behavior without using `new URL` which was not
providing a clear benefit.
@stephanwlee
Copy link
Contributor Author

@falaki We just released 1.12.2 patch with the fix. Thanks for being patient with the release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants