-
Notifications
You must be signed in to change notification settings - Fork 38
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
Refactor frontend pt. 2 #806
Conversation
9728ddb
to
b7b8d7b
Compare
Actual commit finally! I now have npm+webpack mostly working in b7b8d7b. I did this in a minimally obtrusive way for now, which means that I kept all of the files in the same place and only made modifications, which you can see in the diff. Some of the lines in the diff can be removed now that I understand webpack better, which is part of the plan for tomorrow. Importantly, many lines are removed now since we no longer need what was in the With that commit, I can now start up One other issue that came up is that the JS bundle generated by webpack is ... bigger than I would have hoped -- about 2.4 MB. This is not minified at all, so we can compress that down significantly, but it makes me wonder how we should deal with it in the repository... In the ideal case, I think I would force Nengo GUI devs to install node.js so they can do So, my next orders of business for tomorrow are to:
Once those things are done, I will do some thinking about where best to move files. I think it'll be a lot more clear if we keep our JS and Python source files separate, instead of nested as they are now, aside from the generated webpack bundle which will be placed in the Python source tree. That should be easily done, just requires some agreement on the best place and the best names for directories and such. |
a56bf72
to
3c5c13b
Compare
I managed to fix the issue with the new jqueryfiletree. We were using an I was also able to somewhat mitigate the issue of the bundle being quite large... previously it was about 2.6 MB, but I now have a In any case, my next order of business is to move the frontend files outside of the directory tree. Once they've been moved, then I'll start converting the JavaScript files to TypeScript. In getting the files to work with webpack, it became quite clear that with "modular JavaScript" (which is btw what using webpack leads up to), we can make our JS code look pretty similar to Python. More details below. Proposed file movesHere's the current state of the frontend files. The
As you can see, some JS files are four levels deep, and they all exist in the I'm thinking that we move the In addition to moving Here's what it will look like when it's moved. I've done the move locally, but have to change some of the paths in
Modular JavaScriptRight now, our code roughly follows the jQuery plugin model. We make a psuedo-namespace called With modular JavaScript, we get two primary benefits:
So, making our files modular is my first priority. I'm going to do that with as few changes to the actual code as possible -- only making the modularity changes and nothing else. However, once that's done, there are two more steps in the frontend refactoring that I want to do: 1) adopt TypeScript to get access to ES6 features, like legit |
What will be the install location of |
There's no reason they can't be, but I see your point. What would suggest as an alternative?
It wouldn't be installed, only the minified, optimized bundle would be installed as |
OK I did a little looking around at what other projects use ... unfortunately none of them stand out to me as being obviously better than other approaches. But here's a roundup of options:
Jupyter notebook uses the One other approach, which I haven't seen anywhere but sort of follows a Java-like directory structure, is a toplevel
Anyway, since it's unclear the best name at the moment, I'll hold off on renaming things and move on to the modular JavaScript stuff next instead. |
I think I like the top-level names |
Separating concernsOne difficulty in editing the frontend parts of Nengo GUI is that some of the frontend objects are created in the backend. There are a few downsides to this, some of which are important to us and some of which are not. For one, it means that developers have to know how to read/write both frontend (JS) and backend (Python) code -- this doesn't matter so much to us now, but in the future we may attract more specialists. However, an issue that does matter a fair bit for us is that it makes our frontend code difficult to test. I'll give an example here which will hopefully also explain what I mean by separating concerns: creating a value plot. When you click on the "Value" entry in the right click menu, here's what currently happens:
To some extent, these steps are all necessary, but following the paths through frontend and backend code to determine what is called when is difficult for mere mortal programmers, which in turn makes it quite difficult to test. Even worse, there are some parts of the frontend code, like setting the size of an object, that are done directly in the Aside: AJAX, REST and other silly acronymsIt's important to note here that this wasn't done for arbitrary "hey let's make this more difficult than it needs to be" reasons. These steps are necessary because of the asynchronous nature of JavaScript and client-server communication. JavaScript and the underlying technologies are designed for the internet, where you might be waiting for seconds to hear back from the server when you fire off a request for information. If JavaScript stopped everything it's doing while waiting for the server, it would take a ton of time to render even the simplest of modern websites. So, JavaScript fires off a bunch of requests to the server asynchronously, then uses callback functions to handle the responses that the server gives back whenever it's able. Most frequently, those requests are made with Nowadays, client and server code is straightforward to write if you use AJAX because everyone uses AJAX. On the client side, your callbacks are local to the AJAX request you're making, so you can structure your frontend code in a clear way. On the server side, AJAX requests are routed to the appropriate server function using URL components (e.g., Github knows what content to serve on this page because of the However, Nengo GUI doesn't use AJAX, nor should it; while AJAX's asynchronous nature makes it a natural fit for the internet, it still uses the HTTP protocol to transfer information, which imposes a lot of overhead when you're trying to push a huge amount data from the server to the client. Instead, we use a relative newcomer in web technologies to move data: websockets. However, being a new technology, the tooling around websockets is not as mature as it is for AJAX, nor is there a good set of conventions for writing client or server code using websockets. In the end, it means that we are essentially implementing the generic websocket routing and the Nengo GUI specific logic in the same place. Separating concernsOK, back to separating concerns. One set of concerns to try to keep separate are the generic websocket-y stuff and the core business logic. We have done a good job keeping those things separate in the backend server code, helped in part by #673 (which is to be expected given that we all have much more Python experience than JS experience). However, we need to do a better job of this in the frontend. Another set of concerns is the coupling between frontend and backend. Who is in control ? I think because we are more familiar with Python, we have attempted to keep the backend in control of the application as much as possible. However, part of the reason that AJAX took off and became so popular is because it puts the frontend in control, which gives it much more flexibility in terms of providing a good user experience, which is in the end what we want to do. So, I think we need to switch control to the frontend for most aspects of the application. There are a few concrete ways in which this manifests itself:
Examples of these two rules in action: Right now, when the server loads up the page, it loads the template After my refactoring, the control will be in JavaScript's hands. When The separation of concerns here gives us several benefits.
Specifically on the testing front, it would be possible to construct and test the frontend without communicating with the server at all; instead, we stub out what we would expect from the server and substitute mock data. While this is somewhat feasible with the current server, it requires intimate knowledge of the server, and therefore again requires that frontend devs also know how to read and write backend code. RPC over websocketsI believe one way in which we can separate these concerns in the frontend is to use remote procedure calls, or RPCs, over websockets. The idea here is to essentially use websockets like AJAX calls, only without the overhead of making HTTP requests; it essentially does the generic routing operations that we would want to do in most cases. I've found several implementations of this out there so far... the web application messaging protocol (WAMP) and associated tools (Autobahn and crossbar.io) are heavily developed with lots of features, but introduce relatively large dependencies. rpc-websocket gives us just the features we'd need, but is not as heavily developed. My next order of business, after finishing up the modularization of the current JS codebase, is to investigate these RPC implementations and either choose one, or take the good parts from these and implement our own (rpc-websocket is only a few hundred lines of JS, so it's definitely doable). As a brief update of the current situation, the modularization is mostly done; the only thing left is to update the server code to emit what is now the correct JavaScript (hence why I have been thinking about separating these concerns 😉). |
Interesting. Thank you for the detailed description of your thought process! I definitely agree that a lot of the thinking behind the old design was to keep the server side in control as much as possible, because we're much more familiar with Python and its tools. And if modern JS tools make it cleaner to move the client to be in control, that sounds good to me. Thank you for digging into this! |
1560a9c
to
964a4ea
Compare
Modularizing JS is now complete! I've tried out pretty much everything I can think of in the GUI, and it works properly as of 964a4ea. I still have to write a decent commit message for that, but feel free to look at that commit now to see what I mean by modularizing JS... mostly it means not relying on global state, but because the frontend and backend are not yet fully decoupled, we still need to keep track of some global state, which is now encapsulated in the In addition, all of the new commits leading up to 964a4ea do fix some bugs I found and do various things to make frontend development easier. For example, running JSCS and generating the jsDoc is now trivial once I also have not yet gone through and changed the test files. I'm somewhat hesitant to do so, as the tests will definitely need to be changed again by the end of this PR -- and with any luck, a lot of the testing can move to pure JS, eliminating the need for Selenium in the tests we run on TravisCI (though we should keep Selenium tests around for running before releases, etc.) I'll look at them and see how much needs updating, and if it's a lot then I'll skip the tests for now. The next steps are to write that commit message, then look into finishing the decoupling. However, one of my other goals in this refactoring is to switch over the JavaScript code to TypeScript. Because TypeScript compiles to JavaScript, I should be able to do this change without actually modifying the JavaScript files. I'm thinking that actually doing the decoupling is going to be easier with TypeScript, so that's going to be my next task -- in part to confirm whether or not switching over now will make the decoupling easier. |
2eb792d
to
34dbc74
Compare
OK, I'm finally back to work on this PR. I've now converted all the JavaScript files to TypeScript in 34dbc74. Like with the initial commit using npm and Webpack, I've attempted to do this with minimal changes so that the conversion to TypeScript is as straightforward as possible. While the main reason for switching to TypeScript is to give us access to new language features (which 34dbc74 does not do), already the switchover has forced me to fix a few bugs that would have been difficult to spot otherwise. I'll talk about that and give a more thorough introduction to TypeScript tomorrow. |
JavaScript as a low-level languageBefore introducing TypeScript, it's worth giving a bit more context on the evolution of JavaScript, at least from my perspective. JavaScript was famously created in 10 days 20 years ago as a way to add dynamic elements to websites that would execute in the browser (i.e., the client), rather than on the computer serving the website to the browser (i.e., the server). Since the language's "platform", so to speak, is the browser, it was designed with a very different set of concerns than most languages:
JavaScript today is not exactly what it looked like 20 years ago, but it's not very far off. Rather than making significant changes to the language itself (as has happened with Python), JavaScript's evolution has happened through 1) the community discovering the good parts of JavaScript, 2) high-quality open source packages using those good parts to make it easy to use JavaScript in novel ways (huge one-page websites, on the server with node.js, etc) and 3) browser developers optimizing the pants off of JavaScript to make it crazy fast. A good example of how far JavaScript's come through these three avenues is asm.js and tools that use it. asm.js is a subset of JavaScript (i.e., some specific good parts) that runs at nearly-native speeds (2x slower than the equivalent C program), thanks to today's JavaScript engines. The emscripten project can compile LLVM bitcode (which can be generated for many languages including C and C++) to asm.js. Run emscripten on the MAME emulator, and now you can run hundreds of arcade games directly in your browser. People rag on JavaScript a lot for being a bad language that was clearly designed in just 10 days with no forethought. And that's mostly true, but its unique set of concerns and high quality JavaScript engines (i.e., browsers) also give it some unique advantages. In order to best use JavaScript's unique advantages, many people have largely stopped thinking of JavaScript as a high-level language to be written directly, and instead think of it as a low-level language that should be generated by a high-level language that is easier for humans to write. There have been lots of projects that compile (or, perhaps more accurately, "transpile") other languages (including Python) to JavaScript. However, these approaches are a bit short-sighted. First, there are situations where writing raw JavaScript is the right approach, and it's awkward to have a mix of languages in one project. Second, there are lots of high-quality JavaScript libraries out there, and we want to interact with them in an easy way. Third, there are actually innovations on the way for JavaScript the language, so when browsers are all supporting those new language features, it might be more enticing to switch back to JavaScript. In a way, what would be ideal is to write human-readable JavaScript and transpile to well-optimized JavaScript. TypeScriptTypeScript gives us the benefit of writing in a human-readable high-level language and transpiling to low-level JavaScript, yet it avoids the downsides of other projects because TypeScript is a superset of JavaScript. This means that we essentially get to write human-readable JavaScript using new JavaScript features and transpile down to JavaScript that will work on current browsers. Probably the most relevant new JavaScript feature for us is the introduction of the One additional upcoming JavaScript feature is one that this PR has already discussed at length: modules. In addition to the benefits of modularization that I've already discussed, we no longer we have to worry about global variables that may or may not exist as the TypeScript compiler emits far more warning and error messages than webpack, and in general will disallow the use of global variable. The TypeScript module syntax is slightly different than CommonJS's, but in my opinion it is better -- at the very least, it is much more like Python's import syntax. Type definitionsWhat I've described thus far is essentially what the Babel project does -- enable future JavaScript features in current JavaScript. Going with Babel would be a perfectly fine choice. However, TypeScript also allows us to add type definitions to our code, which is the other primary benefit of switching to TypeScript. In short, type definitions allow us to explicitly mark variables as containing values of a certain type. Adding type information seems out of place in the dynamically typed world of JavaScript and Python. Dynamic typing is great, and frees us from lots of unnecessary process in short scripts up to a thousand or so lines. However, when you start scaling up to larger applications (which I would argue the GUI has become), type information becomes very powerful, if not necessary. With larger applications, you're often interacting with parts of the code you did not write, or with third-party libraries. Often, you can glean a lot about how a function works if you know its name, the types of the arguments it accepts, and the type of the value it returns -- which is exactly what a TypeScript type definition is. There's a reason why documentation formats like jsDoc ask you to provide type information. And to preempt comments about how Python has managed to live thus far without it, Python docs often ask for type information, Nengo's But I digress; the concrete benefits of adding type information for most developers is to enable code completion in editors (this is very difficult and/or slow in JavaScript) and to enable sanity checking in the TypeScript compiled (e.g., if you make a typo accessing a property of a class, the compiler will catch it and raise an error). Without that sanity checking or a robust test suite, you basically have to load the page and cross your fingers in JavaScript. The specifics of how to include type information are detailed elsewhere (TypeScript quickstart is a good place to start quickly), but here are some really important concepts that aren't well spelled out in the docs:
If you've done any C or C++ development, then a very apt analogy is the following:
As I mentioned, for your own TypeScript code, the compiler will spit out the TypingsThe answer is: the community (usually). A whole ecosystem of projects (i.e., a bunch of packages and, yes, package managers) grew around providing type definitions for popular JavaScript libraries. The most well known is DefinitelyTyped, which has type definitions for nearly 2000 projects. A tool called However, as often happens with a centralized project that grows so large, a decentralized alternative was created, which is now the preferred option. It's called For the time being, the set of steps involved in dealing with Nengo GUI increases by two due to the need to get
Note that I've used While these extras steps are somewhat annoying, it's important to point out that they are -- hopefully -- temporary. In addition to the Writing
|
So, if one were to jump in one this occasionally to help you with this monumental, but supremely valuable task:
|
It's not quite at the point where people can jump in, unfortunately; I have a couple thousand lines currently uncommitted because I'm ensuring that the GUI still works on each commit. Once it's in a state that works again I can think about places where people can pitch in, but this is the kind of task that is mostly about making sure all of the parts of the code work well in tandem, so it's hard to split off tasks that can be done in isolation. |
34dbc74
to
b13b815
Compare
This post will be a status update, rather than something educational! It's been a few weeks since my last post, so I wanted to check in, as this is still very high on my priority list. I figure that more people will want to start hacking on the GUI come the fall term, so I want to get this mostly wrapped up over the summer. So what have I been doing? Well, I started by fixing the indentation in all the TypeScript files, as I mentioned at the end of my last post (see adf698c). With that "blank slate," I next looked into fixing up the TypeScript files in mechanical ways -- that is, getting the TypeScript files to pass as many automated checks as I could find without doing any real refactoring. The first set of checks were those raised by the TypeScript compiler ( The first project without definitions was The second and third projects were much easier to get working, in part because I had learned a fair bit from writing the One annoying issue that I ran into while writing these definitions -- which I still haven't figured out -- is that I still get compile errors in Emacs even though the project builds properly through webpack. What I've figured out so far is that the Emacs plugin for TypeScript uses TypeScript's Having fixed those issues, I then looked into the current state of linting in TypeScript. Fortunately, this proved much easier to figure out than the type definitions. The de facto linter is TSLint, which integrates easily with Emacs, and can be run on all TypeScript files through the command line. I've added a rule to our With the linter set up, I went through all the source and fixed up the style issues. There was a lot (see the commit, b13b815), so doing it all took a few days in itself... but what ended up being the most time (and energy) consuming was that once it was all lint-free, I built it, started it up, and... the GUI didn't work. At first, I couldn't make any components -- the slider would either appear and be as big as the whole screen then disappear, or it wouldn't appear at all. This ended up being due to a wrong assumption on my part. In the component superclass ( Once I got that working, I could make components, but... no data was being plotted when I pressed play. In this case, I managed to track it down to the After this, basic functionality in the GUI was restored, so I felt okay making a commit. I know for a fact that some parts of the GUI are not working correctly (pressing space in the editor plays/pauses the simulation, for example), but the datastore tests reminded me of how ridiculously useful having a test suite is when making so many big changes. So, rather than pull my hair out manually testing the GUI and debugging issues that I find, I'm going to take a step back and start writing tests for the rest of the frontend code. This will likely be necessary for the actual refactoring anyhow. So, my next post will be another educational one, and it will be about testing in modern frontend development. Like all of the other headaches I've talked about above, getting tests to run in modern frontend dev is far from trivial... but once you somehow happen upon the right combination of tools and set them up properly, everything works very nicely. More about that soon-ish! |
I think most debugging is about identifying wrong assumptions. 💭
I'm pretty sure that all of that size calculation stuff needs some refactoring. There is a bunch of coordinate systems (relative to the parent object, on screen pixels, etc) and it is often not clear what is expressed in what coordinate system. There are three functions to get the size of a netgraph item (
Thank you! ❤️ |
Yes, agreed, Terry's said the same in the past. I'll have to dig in more to think about how best to do this; my only experience with that kind of stuff is from Matplotlib's transformations system, but it's unclear at the moment whether something like that would be appropriate for the GUI. |
Using the <em> tag caused an issue with newer versions of jqueryfiletree because they look at the direct parent of the element that got a click event; since that element was the <em> tag, the parent was <a>, but jqueryfiletree expected that to be the <li> element, and therefore could not determine if the <li> corresponded to a file or directory. Fixed this by removing the <em> tag from the built-in examples directory entry in the file tree, and instead use a CSS rule to make that link italicized (but not any descendants).
Unit tests still pass, with minor modifications. This code could look a lot better if we weren't bound to the APIs in the standard library, which require subclassing.
Making sure work isn't lost
Just have to test them all...
Done up to raster. So far interface is feeling good.
Currently only tested with default.py.cfg; likely some rough edges
Editor text is filled in, and other parts show up. Now just a matter of handling the startup calls between the server and client. Currently working on toolbar and sidebar.
All better. Yep.
As was discussed in a recent meeting, I've split this refactoring (which is perhaps more accurately a rewrite) into a separate repository. Please go there if you're interested in working on it. Not 100% sure how we'll deal with merging or switching over at some point, but splitting it off into a separate repo doesn't limit our options, I believe. |
Alright, so merging #673 also merged #805. I made an empty commit in this new PR so that shouldn't happen again.
I'm just going to continue on from where I left off in #805 so refer to that PR for the first part of the story. As before, feel free to click the unsubscribe button on the right to save your inbox.