— Pixels Commander

[ In English, На русском ]

Применяем стандарты кодирования NASA к JavaScript

Jet Propulsion Laboratory — научное учреждение выполняющее большой объем разработок и исследований для NASA. На счету JPL такие известные миссии, как марсоход Curiosity и недавно покинувший пределы солнечной системы зонд Voyager. JPL разрабатывала программное обеспечение для большинства беспилотных миссий по изучению дальнего космоса и других планет при этом высокая степень автоматизации зондов и длительность миссий обусловили бескомпромиссные требования к качеству программ. В результате сформировались рекомендации по написанию кода которые впитали фантастический опыт инженеров NASA и недавно были опубликованы. Так как требования к программам выполняемым на web — платформе постоянно растут и ей доверяются все более критические задачи давайте применим стандарты кодирования NASA для JavaScript / HTML приложений ради увеличения производительности, надежности и во имя лучшего мира.

 

Nasa coding JavaScript

1.Функции должны быть достаточно короткими что бы при печати уместиться на стандартном листе. Обычно это значит не более 60 строк на функцию.

Эта рекомендация отлично подходит и для JavaScript. Декомпозированный код гораздо проще воспринимать, тестировать и поддерживать.

2.Ограничьтесь простыми управляющими конструкциями. Не используйте goto setjmp, longjmp, а так же прямую или косвенную рекурсию.

Cовет из мира C заставляет задуматься. Мы определенно не будем использовать goto или setjmp в JS, но как быть с рекусией? Почему NASA выступает против простейшей техники преподаваемой еще в школе? Причина кроется в статических анализаторах применяемых NASA для уменьшения вероятности ошибок. Для этих анализаторов рекурсии делают код менее предсказуемым. В JS другая ситуация и существующие анализаторы не предписывают уклоняться от использования рекурсий.Что мы можем вынести из этого совета?

  • Используйте оправданно простые конструкции. Если хотите писать очень надежный код — бросьте писать очень хитрый код и начните писать предсказуемый. Установите стандарт кодирования и соблюдайте его;
  • Используйте анализаторы позволяющие уменьшить вероятность ошибок: JSHint/JSLint/Google Closure Tools;
  • Поддерживайте репозиторий в здоровом состоянии с помощью метрик: Plato;
  • Анализируйте типы с помощью Flow/Google Closure Tools.

3.Не используйте динамическое выделение памяти после инициализации модуля.

На первый взгляд JavaScript движок сам управляет памятью, а проходящий время от времени garbage collection решает все наши проблемы. Это не так. Память в JS имеет свойство течь, garbage collection не управляем и уменьшает производительность системы во время работы, а избалованные веб — программисты не имеют культуры управления памятью. В результате из этого правила можно выделить три рекомендации. Первые две будут вполне разумной практикой в любом проекте, третий только в проектах требующих предсказуемого расхода памяти и максимальной производительности.

  • С уважением относитесь к переменным, упорядочивайте их объявление. Если позволяет стандарт кодирования — объявляйте переменные в начале изолированного блока, это улучшит видимость использования.
  • Следите за утечками памяти, очищайте ссылки на переменные, убирайте слушатели, избегайте создания замыканий где это возможно. Классическая статья.
  • Можно заставить JS работать в режиме статического выделения памяти и сделать garbage collection управляемым (а значит избежать падения производительности и sawtooth pattern) с помощью пула объектов.

4.Все циклы должны иметь четко заданный верхний предел.

По объяснению JPL это делает статический анализ еще более эффективным и позволяет избежать бесконечного выполнения цикла. В случае если лимит на повторение превышен — функция возвращает ошибку и таким образом выводит систему из неожиданного состояния. Что же, вполне логично для программы с up-time 20+ лет! Проверка на превышение лимита осуществляется с помощью утверждений (assertions) подробнее о которых в пункте пятом. Если вы полностью примете парадигму assertions — ставьте уже и лимиты на циклы, вам это понравится.

5.Устанавливайте не меньше двух проверок (assertions) на функцию.

Тут нужно немного сказать о проверках. Проще всего представить их, как юнит — тесты, выполняемые по ходу исполнения программы.

if (!c_assert(altitude > MAX_POSSIBLE_ALTITUDE) == true) {
    return ERROR;
}

Этот раздел стандарта буквально гласит следующее:

«Статистически на каждые 10 — 100 строк кода приходится один дефект найденный с помощью юнит тестов. Чем больше плотность тестов — тем больше дефектов мы можем обнаружить автоматически.»

Отлично, значит мы можем расценивать это правило, как «Пишите юнит тесты!«? И да и нет.
Особенностью проверок и их приемуществом является постоянный контроль за состоянием системы по ходу выполнения. Самым близким аналогом этой практики для JavaScript будет совмещение юнит тестов и run-time контроля данных с генерацией ошибок при отклонениях.

  • Чем выше плотность тестов, тем меньше дефектов. Минимальное количество тестов — 2 на функцию;
  • Следите за состоянием системы в run-time, генерируйте ошибки в случае неполадок и обрабатывайте их.

6.Переменные должны располагаться в самом нижнем скоупе из возможных.

Правило рождено желанием поместить данные в приватное пространство и оградить их от записи извне. Разумная и простая практика.

7.Возвращаемое значение любой не — void функции должно проверяться вызывающей функцией. Все входящие аргументы функций должны проверяться.

По заверению авторов рекомендаций это правило нарушается чаще остальных, ведь в самой строгой интерпретации оно требует проверки результата даже встроенных функций.
Мое мнение — стоит проверять возвращаемые значения third — party библиотек, а так же входящие аргументы функций на существование и соответствие ожидаемому типу.

8.Использование препроцессора C должно быть ограничено включением заголовков и простыми определениями. Препроцессор — мощный инструмент, однако приводящий к уменьшению читабельности и дополнительным затратам на тестирование.

Вынужден согласиться — использование препроцессоров должно быть ограниченно в любых языках. Они не нужны пока есть стандартный, понятный и надежный синтаксис для ввода команд в машину, который к тому же активно развивается. Надежный и быстрый JavaScript стоит писать на JavaScript… Интересный материал о стоимости транспайлинга JS

9.Использование указателей должно быть ограниченно. Указатели на функции запрещены.

Это то правило из которого JavaScript разработчик не сможет вынести пользы.

10.С самого первого дня разработки код должен успешно проходить тесты и статический анализ с самыми строгими, педантичными параметрами проверки. При этом не должен вызывать преудпреждающих сообщений и тем более сообщений об ошибках.

Мы все это знаем… Не копите синтаксические замечания, не откладывайте фиксы. Держите код чистым, а перфекциониста в себе живым!

«Эти десять правил были экспериментально использованы в JPL при написании критического ПО с воодушевляющими результатами. После преодоления здорового сопротивления столь строгим правилам разработчики находят их полезными, приводящими к лучшей надежности, читабельности и тестируемости кода. Если сначала правила выглядят драконовскими, то вы должны понимать — они позволяют писать лучший код там, где от него могут зависеть жизни людей: ПО для контроля самолетов, ядерных электростанций находящихся в нескольких километрах от вашего дома или космических кораблей доставляющих космонавтов на орбиту. Правила работают, как ремень безопасности в вашей машине — сначала они мешают, но со временем становятся вторым Я которое невообразимо игнорировать.»(с) Gerard J. Holzmann, автор «The Power of Ten — Rules for Developing Safety Critical Code»

Заключение

Начиная писать эту статью, я не представлял как много мы можем перенять у NASA. Сравнение виделось нелепым сродни сравнению опытного инженера и школьника — самоучки, но это не так. Открытием стала зрелость web — платформы: существующий уровень проектной инфраструктуры, уже устоявшееся использование лучших практик из «старших» языков и изученность возможностей поражают. Хотя так много еще можно достичь…

  • rayC

    Restriction on recursion from NASA’s point of view is because of limited stack space. Every time you recurse, you put state information on the stack. Do you know how many times you can recurse before you run out of stack?

  • Аноним

    Good point which was not completely covered in the rules. Actually I never saw a good publication on how call stacks impact JS performance or reliability. Should figure it out.

    As for maximum call stack in JS — there is good note on it http://www.2ality.com/2014/04/call-stack-size.html

  • Victor Homyakov

    Visualization of call stack depth for different stack frame sizes: http://jsfiddle.net/9YMDF/9/ and summary for different browsers: http://thinkjs.blogspot.com/2014/08/javascript-2.html

  • Fran

    Actually, this isn’t so hard to do… Especially if you know well design patterns…. Nice recommendations 🙂

  • Nat

    Rule 9 actually could be applied to JavaScript. I would ban nested data structures and passing functions to other functions. You could write JS this way, and it would make the code easier to reason about in some ways, but it would probably be a bad idea.

  • Аноним

    Yes, there is so much buzz around functional programming nowadays that I would avoid making such a suggestion. Actually functions are a cornerstone of JavaScript development so this part of #9 is not applicable. But as for another part I`ve got a nice hint on http://en.wikipedia.org/wiki/Law_of_Demeter so still something to consider.

  • ryan

    You would? Why would you do that?

  • Ryan Scheel

    Nat doesn’t actually mean that their code is written that way. Rather, to truly follow rule #9, one would have to write that way. There’s an implicit «follow rule #9 by» following the word «would», and the next sentence states that it’s a bad idea (probably). Which, given the constraints these rules impose, usage of JS at all would be a bad idea.

  • http://temporal.pr0.pl/devblog Jacek Złydach

    A lot of those rulese have to do with not just correctness, but predictable execution time. At NASA they’re writing a lot of real-time applications which have strict constraints on how long a given piece of code can run.

    In particular, function pointers will tend to make reasoning about execution time difficult, because you can’t be sure what code gets executed when you call a function indirectly.

    Also note that the rule about no function pointers pretty much kills any kind of polymorphism, if you were trying to do OOP in C.

  • Dilbert

    I disagree with your function pointer remark regarding fuzzy execution times. Replacing it with a large switch-case instead would hardly make the branch predictor any happier.

  • http://temporal.pr0.pl/devblog Jacek Złydach

    I didn’t mean this as a branch prediction issue but as a problem for programmer / static analysis tool. When you call a function through a function pointer, you basically have no idea what code will actually get executed. You’d have to manually inspect every place that sets the value of the function pointer, and even that might not help if some of those places get executed indirectly through another function pointer.

    And God forbid, if the function pointer is set to a value determined at runtime from external input…

  • http://www.dullroar.com/ Jim Lehmer

    Actually, in JS you could look at #9 as «Do not de-reference more than one ‘dot’ in a statement.» In other words, assuming you know foo is defined, then trying to dereference foo.bar is OK, but it is not OK to blindly do foo.bar.baz.xyzzy.plugh, for the possibility of hitting something undefined along the way.

  • Filipe Silva

    Was thinking the exact same thing!

  • Anton B.

    This is one of those things…
    The x = (y != null && y.z != null & y.z.w != null) ? y.z.w.value : null;

    In the latest version of C#, Microsoft has added a really nice short hand for this:

    x = y?.z?.w?.value ?? null;

  • http://www.dullroar.com/ Jim Lehmer

    Yeah, I am looking forward to the new null shortcuts in C#.

  • Аноним

    this syntax is available in CoffeScript
    in JS one might use helper function to define namespaces, eg


    // something like this
    var w = defineNS( 'y.z.w', { ...object def } );
    // or this
    y.x.NEW = ensure( 'y.x' );
    // latter might throw proper exception if y.x is undefined

    but both approaches (cf & js strings) seem to be problematic, imho

  • Jon Green

    Rule 2: I agree with RayC about recursion: a point I was going to make myself. I would put a qualifier on that restriction: if you must use recursion, do so only when the recursion limit is strictly bounded, and minimal. But any recursion may be expressed as iteration, so for preference always iterate instead. Whilst recursion’s great for computational theory, it causes problems in the real world because stacks are limited. A classic example of: «In theory, theory and practice are the same thing. In practice, they differ.»

    Rule 3: not using dynamic memory at all (or, at most, only pre-allocating during initialisation) is a classic embedded development technique. Your JavaScript’s reliability will be considerably improved if you can follow this principle, as it will mean garbage collection is unnecessary or minimal, fragmentation does not occur (or to a far smaller extent), and memory use is strictly circumscribed. When you use static allocation all the time, you have an understanding of your memory use that you can never achieve using heap. If you’re using server-side JS, static allocation will definitely improve your long-term robustness.

    Rule 5: this can be rewritten as: «Test your assumptions». Writing assertions is more than just creating a unit test: it’s a documentation of what you have assumed about entry conditions; of what you were thinking when the function was written. The act of writing them down helps clarify them in your mind, and explain them to developers who are approaching the code «cold». So, whilst they provide a degree of code robustness testing, they also provide code clarity.

    Rule 7: in C or C++, it’s common (and good) practice to cast to (void) the result of a function you don’t need to test. This documents that you’re deliberately disregarding the result, and haven’t simply forgotten to check it. I forget if it’s possible to do the same in JS, but as an alternative I suppose you could write «function IGNORED(x) { }» and use that to wrap function invocations where you choose to discard a known return value.

    Rule 8: I slightly disagree with this rule. There is a principle in Smalltalk: «Code it once.» By putting a segment of frequently-used code in a macro, you can change all instances at a time if there’ a bug. However, in many cases it’s tidier to write it as a function, and (if relevant) use the «inline» qualifier. And it’s certainly true that preprocessor macros are non-trivial to debug, so they should be used carefully and sparingly.However, the relevant of this rule to JS is limited, so I’ll leave it there.

    Rule 9: this is relevant to JS. Function pointers can be created and assigned: «var foo = function () { return «A»); }». However, whether there’s value (for JS) in limiting the use of (as in this case) lambda functions is debatable. I’d suggest that this rule — at least as regards function pointers — is probably more relevant to C/C++ than to JavaScript.

  • Dilbert

    Regarding #8, you can usually simply write a static inline function directly in the .h file and have it behave like a macro with added type safety.

  • Jon Green

    Um…that’s what I said. (And the only point of putting it in a header is if it’s shared, otherwise you’re just muddying your external interfaces.)

  • Dilbert

    NASA guidelines for embedded code don’t really apply too well to javascript.

    2. «Simple constructs» cannot possibly apply to javascript. It’s a prototypal, dynamic language. You don’t code microcontrollers in javascript, after all. Passing a function with a closure is so common in JS, and would probably make a plain C embedded programmer crazy.

    3. «Static allocation»? No way to eliminate completely. Object pools in JS are used for performance reasons — to avoid GC. The NASA article talks about plain C style memory allocation, where you want to avoid fragmenting your memory in limited memory long-running applications.

    6. «Scope declaration»? Did you know that there is no scope level variable declaration in javascript? Even jslint will warn against such declarations because they are misleading.

  • John Goodwin

    Just some notes on item #5.

    Styling wise, I disagree with checking booleans as === true. Additionally, the ! operator causes an implicit type conversion to boolean so the === to prevent accidental casting comparison anomalies will not be prevented.

    When possible, I also prefer people invert logic so that as few ! operators are required when it is reasonable to do. It is also my opinion that assert statements should throw when assertions fail, not return true/false.

    Revised, your assert should read like this:

    c_assert(altitude <= MAX_POSSIBLE_ALTITUDE);

    I think this is much easier to read and will result in fewer unintended side effects.

    John

  • David

    Great, do this mean that we can treat rule as: “Write unit tests!“?

    Correction: DOES

  • Аноним

    Thanks, fixed