I designed a simple method for this, using console.time() & console.timeEnd():
measure function definition
function measureRunningTime(func,...args){
const varToString = varObj => Object.keys(varObj)[0]
const displayName = func.name || varToString({ func })
console.time(displayName)
func(...args)
console.timeEnd(displayName)
}
To use it, pass a function without arguments, with arguments binded, or with arguments as the following parameters.
Examples:
let’s say I want to check the running time of the simplest searching algorithm — SimpleSearch:
measured function definition (your code here)
const simpleSearch = (array = [1,2,3] ,item = 3) => {
for(let i = 0; i< array.length; i++){
if (array[i] === item) return i;
}
return -1
}
implementation without arguments
measureRunningTime(simpleSearch)
//Prints something like that-> simpleSearch: 0.04ms
implementation with arguments using .bind()
const array = [1,2,3]
const item = 3
measureRunningTime(simpleSearch.bind(null, array, item))
//Prints something like that-> bound simpleSearch: 0.04ms
implementation with arguments without using .bind()
const array = [1,2,3]
const item = 3
measureRunningTime(simpleSearch, array, item)
//Prints something like that-> simpleSearch: 0.04ms
-> Take notice!! this implementation is far from perfect — for example there is no error handling — but it can be used to check the running times of simple algorithms,
Moreover , I’m not an experienced programmer so take everything with a grain of salt 🧂 👌
Сегодня, в шестой части перевода руководства по Node.js, мы поговорим о цикле событий, о стеке вызовов, о функции process.nextTick()
, о таймерах. Понимание этих и других механизмов Node.js является одной из основ успешной разработки приложений для этой платформы.
[Советуем почитать] Другие части цикла
Часть 1: Общие сведения и начало работы
Часть 2: JavaScript, V8, некоторые приёмы разработки
Часть 3: Хостинг, REPL, работа с консолью, модули
Часть 4: npm, файлы package.json и package-lock.json
Часть 5: npm и npx
Часть 6: цикл событий, стек вызовов, таймеры
Часть 7: асинхронное программирование
Часть 8: Руководство по Node.js, часть 8: протоколы HTTP и WebSocket
Часть 9: Руководство по Node.js, часть 9: работа с файловой системой
Часть 10: Руководство по Node.js, часть 10: стандартные модули, потоки, базы данных, NODE_ENV
Полная PDF-версия руководства по Node.js
Цикл событий
Если вы хотите разобраться с тем, как выполняется JavaScript-код, то цикл событий (Event Loop) — это одна из важнейших концепций, которую необходимо понять. Здесь мы поговорим о том, как JavaScript работает в однопоточном режиме, и о том, как осуществляется обработка асинхронных функций.
Я многие годы занимался разработкой на JavaScript, но не могу сказать, что полностью понимал то, как всё функционирует, так сказать, «под капотом». Программист вполне может не знать о тонкостях устройства внутренних подсистем среды, в которой он работает. Но обычно полезно иметь хотя бы общее представление о подобных вещах.
JavaScript-код, который вы пишете, выполняется в однопоточном режиме. В некий момент времени выполняется лишь одно действие. Это ограничение, на самом деле, является весьма полезным. Это значительно упрощает то, как работают программы, избавляя программистов от необходимости решать проблемы, характерные для многопоточных сред.
Фактически, JS-программисту нужно обращать внимание только на то, какие именно действия выполняет его код, и стараться при этом избежать ситуаций, вызывающих блокировку главного потока. Например — выполнения сетевых вызовов в синхронном режиме и бесконечных циклов.
Обычно в браузерах, в каждой открытой вкладке, имеется собственный цикл событий. Это позволяет выполнять код каждой страницы в изолированной среде и избегать ситуаций, когда некая страница, в коде которой имеется бесконечный цикл или выполняются тяжёлые вычисления, способна «подвесить» весь браузер. Браузер поддерживает работу множества одновременно существующих циклов событий, используемых, например, для обработки вызовов к различным API. Кроме того, собственный цикл событий используется для обеспечения работы веб-воркеров.
Самое важное, что надо постоянно помнить JavaScript-программисту, заключается в том, что его код использует собственный цикл событий, поэтому код надо писать с учётом того, чтобы этот цикл событий не заблокировать.
Блокировка цикла событий
Любой JavaScript-код, на выполнение которого нужно слишком много времени, то есть такой код, который слишком долго не возвращает управление циклу событий, блокирует выполнение любого другого кода страницы. Подобное приводит даже к блокировке обработки событий пользовательского интерфейса, что выражается в том, что пользователь не может взаимодействовать с элементами страницы и нормально с ней работать, например — прокручивать.
Практически все базовые механизмы обеспечения ввода-вывода в JavaScript являются неблокирующими. Это относится и к браузеру и к Node.js. Среди таких механизмов, например, можно отметить средства для выполнения сетевых запросов, используемые и в клиентской и в серверной средах, и средства для работы с файлами Node.js. Существуют и синхронные способы выполнения подобных операций, но их применяют лишь в особых случаях. Именно поэтому в JavaScript огромное значение имеют традиционные коллбэки и более новые механизмы — промисы и конструкция async/await.
Стек вызовов
Стек вызовов (Call Stack) в JavaScript устроен по принципу LIFO (Last In, First Out — последним вошёл, первым вышел). Цикл событий постоянно проверяет стек вызовов на предмет того, имеется ли в нём функция, которую нужно выполнить. Если при выполнении кода в нём встречается вызов некоей функции, сведения о ней добавляются в стек вызовов и производится выполнение этой функции.
Если даже раньше вы не интересовались понятием «стек вызовов», то вы, если встречались с сообщениями об ошибках, включающими в себя трассировку стека, уже представляете себе, как он выглядит. Вот, например, как подобное выглядит в браузере.
Сообщение об ошибке в браузере
Браузер, при возникновении ошибки, сообщает о последовательности вызовов функций, сведения о которых хранятся в стеке вызовов, что позволяет обнаружить источник ошибки и понять, вызовы каких функций привели к сложившейся ситуации.
Теперь, когда мы в общих чертах поговорили о цикле событий и о стеке вызовов, рассмотрим пример, иллюстрирующий выполнение фрагмента кода, и то, как этот процесс выглядит с точки зрения цикла событий и стека вызовов.
Цикл событий и стек вызовов
Вот код, с которым мы будем экспериментировать:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
bar()
baz()
}
foo()
Если этот код выполнить, в консоль попадёт следующее:
foo
bar
baz
Такой результат вполне ожидаем. А именно, когда этот код запускают, сначала вызывается функция foo()
. Внутри этой функции мы сначала вызываем функцию bar()
, а потом — baz()
. При этом стек вызовов в ходе выполнения этого кода претерпевает изменения, показанные на следующем рисунке.
Изменение состояния стека вызовов при выполнении исследуемого кода
Цикл событий, на каждой итерации, проверяет, есть ли что-нибудь в стеке вызовов, и если это так — выполняет это до тех пор, пока стек вызовов не опустеет.
Итерации цикла событий
Постановка функции в очередь на выполнение
Вышеприведённый пример выглядит вполне обычным, в нём нет ничего особенного: JavaScript находит код, который надо выполнить, и выполняет его по порядку. Поговорим о том, как отложить выполнение функции до момента очистки стека вызовов. Для того чтобы это сделать, используется такая конструкция:
setTimeout(() => {}), 0)
Она позволяет выполнить функцию, переданную функции setTimeout()
, после того, как будут выполнены все остальные функции, вызванные в коде программы.
Рассмотрим пример:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()
То, что выведет этот код, возможно, покажется неожиданным:
foo
baz
bar
Когда мы запускаем этот пример, сначала вызывается функция foo()
. В ней мы вызываем setTimeout()
, передавая этой функции, в качестве первого аргумента, bar
. Передав ей в качестве второго аргумента 0
, мы сообщаем системе о том, что эту функцию следует выполнить как можно скорее. Затем мы вызываем функцию baz()
.
Вот как теперь будет выглядеть стек вызовов.
Изменение состояния стека вызовов при выполнении исследуемого кода
Вот в каком порядке теперь будут выполняться функции в нашей программе.
Итерации цикла событий
Почему всё происходит именно так?
Очередь событий
Когда вызывается функция setTimeout()
, браузер или платформа Node.js запускает таймер. После того, как таймер сработает (в нашем случае это происходит немедленно, так как мы установили его на 0), функция обратного вызова, переданная setTimeout()
, попадает в очередь событий (Event Queue).
В очередь событий, если речь идёт о браузере, попадают и события, инициированные пользователем — события, вызванные щелчками мышью по элементам страницы, события, вызываемые при вводе данных с клавиатуры. Тут же оказываются обработчики событий DOM вроде onload
, функции, вызываемые при получении ответов на асинхронные запросы по загрузке данных. Здесь они ждут своей очереди на обработку.
Цикл событий отдаёт приоритет тому, что находится в стеке вызовов. Сначала он выполняет всё, что ему удаётся найти в стеке, а после того, как стек оказывается пустым, переходит к обработке того, что находится в очереди событий.
Нам не нужно ждать, пока функция, наподобие setTimeout()
, завершит работу, так как подобные функции предоставляются браузером и они используют собственные потоки. Так, например, установив с помощью функции setTimeout()
таймер на 2 секунды, вы не должны, остановив выполнение другого кода, ждать эти 2 секунды, так как таймер работает за пределами вашего кода.
Очередь заданий ES6
В ECMAScript 2015 (ES6) была введена концепция очереди заданий (Job Queue), которой пользуются промисы (они тоже появились в ES6). Благодаря очереди заданий результатом выполнения асинхронной функции можно воспользоваться настолько быстро, насколько это возможно, без необходимости ожидания очищения стека вызовов.
Если промис разрешается до окончания выполнения текущей функции, соответствующий код будет выполнен сразу после того, как текущая функция завершит работу.
Я обнаружил интересную аналогию для того, о чём мы сейчас говорим. Это можно сравнить с американскими горками в парке развлечений. После того, как вы прокатились на горке и хотите сделать это ещё раз, вы берёте билет и становитесь в хвост очереди. Так работает очередь событий. А вот очередь заданий выглядит иначе. Эта концепция похожа на льготный билет, который даёт вам право совершить следующую поездку сразу после того, как вы закончили предыдущую.
Рассмотрим следующий пример:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
new Promise((resolve, reject) =>
resolve('should be right after baz, before bar')
).then(resolve => console.log(resolve))
baz()
}
foo()
Вот что будет выведено после его выполнения:
foo
baz
should be right after baz, before bar
bar
То, что тут можно видеть, демонстрирует серьёзное различие промисов (и конструкции async/await, которая на них основана) и традиционных асинхронных функций, выполнение которых организуется посредством setTimeout()
или других API используемой платформы.
process.nextTick()
Метод process.nextTick()
по-особому взаимодействует с циклом событий. Тиком (tick) называют один полный проход цикла событий. Передавая функцию методу process.nextTick()
, мы сообщаем системе о том, что эту функцию нужно вызвать после завершения текущей итерации цикла событий, до начала следующей. Использование данного метода выглядит так:
process.nextTick(() => {
//выполнить какие-то действия
})
Предположим, цикл событий занят выполнением кода текущей функции. Когда эта операция завершается, JavaScript-движок выполнит все функции, переданные process.nextTick()
в ходе выполнения предыдущей операции. Используя этот механизм, мы стремимся к тому, чтобы некая функция была бы выполнена асинхронно (после текущей функции), но как можно скорее, без постановки её в очередь.
Например, если воспользоваться конструкцией setTimeout(() => {}, 0)
функция будет выполнена на следующей итерации цикла событий, то есть — гораздо позже, чем при использовании в такой же ситуации process.nextTick()
. Этот метод стоит использовать тогда, когда нужно обеспечить выполнение некоего кода в самом начале следующей итерации цикла событий.
setImmediate()
Ещё одной функцией, предоставляемой Node.js для асинхронного выполнения кода, является setImmediate()
. Вот как ей пользоваться:
setImmediate(() => {
//выполнить некий код
})
Функция обратного вызова, переданная setImmediate()
, будет выполнена на следующей итерации цикла событий.
Чем setImmediate()
отличается от setTimeout(() => {}, 0)
(то есть, от таймера, который должен сработать как можно скорее) и от process.nextTick()
?
Функция, переданная process.nextTick()
выполнится после завершения текущей итерации цикла событий. То есть, такая функция всегда будет выполняться до функции, выполнение которой запланировано с помощью setTimeout()
или setImmediate()
.
Вызов функции setTimeout()
с установленной задержкой в 0 мс очень похож на вызов setImmediate()
. Порядок выполнения функций, переданных им, зависит от различных факторов, но и в том и в другом случаях коллбэки будут вызваны на следующей итерации цикла событий.
Таймеры
Выше мы уже говорили о функции setTimeout()
, которая позволяет планировать вызовы передаваемых ей коллбэков. Уделим некоторое время более подробному описанию её особенностей и рассмотрим ещё одну функцию, setInterval()
, схожую с ней. В Node.js функции для работы с таймерами входят в модуль timer, но пользоваться ими можно, не подключая этот модуль в коде, так как они являются глобальными.
▍Функция setTimeout()
Напомним, что при вызове функции setTimeout()
ей передают коллбэк и время, в миллисекундах, по прошествии которого будет вызван коллбэк. Рассмотрим пример:
setTimeout(() => {
// выполняется через 2 секунды
}, 2000)
setTimeout(() => {
// выполняется через 50 миллисекунд
}, 50)
Здесь мы передаём setTimeout()
новую функцию, тут же описываемую, но здесь можно использовать и существующую функцию, передавая setTimeout()
её имя и набор параметров для её запуска. Выглядит это так:
const myFunction = (firstParam, secondParam) => {
//выполнить некий код
}
// выполняется через 2 секунды
setTimeout(myFunction, 2000, firstParam, secondParam)
Функция setTimeout()
возвращает идентификатор таймера. Обычно он не используется, но его можно сохранить, и, при необходимости, удалить таймер, если в запланированном выполнении коллбэка больше нет необходимости:
const id = setTimeout(() => {
// этот код должен выполниться через 2 секунды
}, 2000)
// Программист передумал, выполнять этот код больше не нужно
clearTimeout(id)
▍Нулевая задержка
В предыдущих разделах мы использовали setTimeout()
, передавая ей, в качестве времени, по истечении которого надо вызвать коллбэк, 0
. Это означало, что коллбэк будет вызван так скоро, как это возможно, но после завершения выполнения текущей функции:
setTimeout(() => {
console.log('after ')
}, 0)
console.log(' before ')
Такой код выведет следующее:
before
after
Этот приём особенно полезен в ситуациях, когда, при выполнении тяжёлых вычислительных задач, не хотелось бы блокировать главный поток, позволяя выполняться и другим функциям, разбивая подобные задачи на несколько этапов, оформляемых в виде вызовов setTimeout()
.
Если вспомнить о вышеупомянутой функции setImmediate()
, то в Node.js она является стандартной, чего нельзя сказать о браузерах (в IE и Edge она реализована, в других — нет).
▍Функция setInterval()
Функция setInterval()
похожа на setTimeout()
, но между ними есть и различия. Вместо однократного выполнения переданного ей коллбэка setInterval()
будет периодически, с заданным интервалом, вызывать этот коллбэк. Продолжаться это будет, в идеале, до того момента, пока программист явным образом не остановит этот процесс. Вот как пользоваться этой функцией:
setInterval(() => {
// выполняется каждые 2 секунды
}, 2000)
Коллбэк, переданный функции, показанной выше, будет вызываться каждые 2 секунды. Для того чтобы предусмотреть возможность остановки этого процесса, нужно получить идентификатор таймера, возвращаемый setInterval()
и воспользоваться командой clearInterval()
:
const id = setInterval(() => {
// выполняется каждые 2 секунды
}, 2000)
clearInterval(id)
Распространённой методикой является вызов clearInterval()
внутри коллбэка, переданного setInterval()
при выполнении некоего условия. Например, следующий код будет периодически запускаться до тех пор, пока свойство App.somethingIWait
не примет значение arrived
:
const interval = setInterval(function() {
if (App.somethingIWait === 'arrived') {
clearInterval(interval)
// если условие выполняется - удалим таймер, если нет - выполним некие действия
}
}, 100)
▍Рекурсивная установка setTimeout()
Функция setInterval()
будет вызывать переданный ей коллбэк каждые n
миллисекунд, не заботясь о том, завершилось ли выполнение этого коллбэка после его предыдущего вызова.
Если на каждый вызов этого коллбэка всегда требуется одно и то же время, меньшее n
, то никаких проблем тут не возникает.
Периодически вызываемый коллбэк, каждый сеанс выполнения которого занимает одно и то же время, укладывающееся в промежуток между вызовами
Возможно, для выполнения коллбэка каждый раз требуется разное время, которое всё ещё меньше n
. Если, например, речь идёт о выполнении неких сетевых операций, то такая ситуация вполне ожидаема.
Периодически вызываемый коллбэк, каждый сеанс выполнения которого занимает разное время, укладывающееся в промежуток между вызовами
При использовании setInterval()
может возникнуть ситуация, когда выполнение коллбэка занимает время, превышающее n
, что приводит к тому, что следующий вызов осуществляется до завершения предыдущего.
Периодически вызываемый коллбэк, каждый сеанс выполнения которого занимает разное время, которое иногда не укладывается в промежуток между вызовами
Для того чтобы избежать подобной ситуации, можно воспользоваться методикой рекурсивной установки таймера с помощью setTimeout()
. Речь идёт о том, что следующий вызов коллбэка планируется после завершения его предыдущего вызова:
const myFunction = () => {
// выполнить некие действия
setTimeout(myFunction, 1000)
}
setTimeout(
myFunction()
}, 1000)
При таком подходе можно реализовать следующий сценарий:
Рекурсивный вызов setTimeout() для планирования выполнения коллбэка
Итоги
Сегодня мы поговорили о внутренних механизмах Node.js, о таких, как цикл событий, стек вызовов, обсудили работу с таймерами, которые позволяют планировать выполнение кода. В следующий раз мы углубимся в тему асинхронного программирования.
Уважаемые читатели! Сталкивались ли вы с ситуациями, когда вам приходилось использовать process.nextTick()?
Measuring your apps performance is very important when your code is being used in production. You should therefore know the execution time of your most frequently used functions. Modern browsers and the Node.js platform provide great APIs to measure performance. In this article, I am presenting a few selected ones using JavaScript and TypeScript code examples.
Business Logic
First of all, we need a function that mimics our business logic. To make it simple, I am using a function which will return a value after 2 seconds (2000ms):
function businessLogic(): Promise<number> {
return new Promise((resolve) => {
setTimeout(resolve, 2000);
});
}
Enter fullscreen mode
Exit fullscreen mode
Console Timers (console.time
)
The easiest way to print the execution time of a function to the console, is to use a console timer. Everything that has to be done, is calling console.time
and console.timeEnd
with the same identifier:
(async () => {
console.time('businessLogic');
await businessLogic();
console.timeEnd('businessLogic');
})();
Enter fullscreen mode
Exit fullscreen mode
As a result, we get the execution time printed to our console in a human-readable format (can be milliseconds, seconds, or other resolutions):
businessLogic: 2.012s
Enter fullscreen mode
Exit fullscreen mode
High Resolution Timers (performance.now
)
If you want to customize the output of your profiling, then you can use a high resolution timer like performance.now
. It will return the measured execution time in 1 millisecond increments:
(async () => {
const start = performance.now();
await businessLogic();
const stop = performance.now();
const inSeconds = (stop - start) / 1000;
const rounded = Number(inSeconds).toFixed(3);
console.log(`businessLogic: ${rounded}s`);
})();
Enter fullscreen mode
Exit fullscreen mode
businessLogic: 2.012s
Enter fullscreen mode
Exit fullscreen mode
Time tracking util
You can also build your own utility function to track execution time with performance.now
:
// Util function to track execution time in seconds
export async function trackInSeconds(fn: Function): Promise<string> {
const start = performance.now();
await fn();
const end = performance.now();
const inSeconds = (end - start) / 1000;
return Number(inSeconds).toFixed(0);
}
(async () => {
// Your business logic
const myFunction = () => {
return new Promise(resolve => {
// Function completes after 5s
setTimeout(resolve, 5000);
});
};
const timeInSeconds = await trackInSeconds(myFunction);
console.log(`Execution took ${timeInSeconds} seconds.`);
})();
Enter fullscreen mode
Exit fullscreen mode
Execution took 5 seconds.
Enter fullscreen mode
Exit fullscreen mode
Performance Hooks (perf_hooks
)
Node.js provides performance measurement APIs to profile JavaScript and TypeScript functions. With the perf_hooks
module it becomes very convenient to profile multiple functions at once.
TypeScript Typings
To use the perf_hooks
module with TypeScript, we have to install type definitions that match our Node.js version (I am using v14):
npm install --save @types/node@14
Enter fullscreen mode
Exit fullscreen mode
Performance Observer
We have seen that console.time
doesn’t let us customize the output and performance.now
is very difficult to control if you want to monitor several functions. That’s why Node.js provides a performance observer. The performance observer can listen to different kinds of measurements and receives entries that return the measured time in milliseconds.
To make the performance collection asynchronous, the buffered
flag can be used, so that multiple entries will be buffered internally:
import {PerformanceObserver} from 'perf_hooks';
const observer = new PerformanceObserver(list => list.getEntries().forEach(entry => console.info(entry)));
observer.observe({buffered: true, entryTypes: ['measure']});
Enter fullscreen mode
Exit fullscreen mode
Performance Marks (performance.mark
)
After setting up the performance observer, we can start a measurement. The simplest way is to set markings. It works similar to the console.time
approach with the difference that we need to use different labels for the start and the stop:
import {performance, PerformanceObserver} from 'perf_hooks';
function businessLogic(): Promise<number> {
return new Promise((resolve) => {
setTimeout(resolve, 2000);
});
}
(async () => {
const observer = new PerformanceObserver(list => list.getEntries().forEach(entry => console.info(entry)));
observer.observe({buffered: true, entryTypes: ['measure']});
performance.mark('start');
await businessLogic();
performance.mark('stop');
performance.measure('Business Logic', 'start', 'stop');
})();
Enter fullscreen mode
Exit fullscreen mode
💡 Please note that our observer listens to entries of type 'measure'
.
Output:
PerformanceEntry {
name: 'Business Logic',
entryType: 'measure',
startTime: 3020.9561,
duration: 2007.4025
}
Enter fullscreen mode
Exit fullscreen mode
Performance Instrumentation (performance.timerify
)
For more convenience, there is the performance.timerify
function. It wraps new functions automatically into performance marks, so that we don’t need to declare start and stop. In that case our observer must listen to the entry type 'function'
:
(async () => {
const observer = new PerformanceObserver(list => list.getEntries().forEach(entry => console.info(entry)));
observer.observe({buffered: true, entryTypes: ['function']});
const wrapped = performance.timerify(businessLogic);
await wrapped();
})();
Enter fullscreen mode
Exit fullscreen mode
PerformanceEntry {
name: 'businessLogic',
entryType: 'function',
startTime: 2221.5801,
duration: 0.6079
}
Enter fullscreen mode
Exit fullscreen mode
💡 As you can see, the tracked duration
is different from our measurements with performance.mark
. That’s because performance.timerify
doesn’t work out of the box with asynchronous functions on Node.js v14.
James M Snell from the Node team tweeted me that performance.timerify
will work with async functions in Node v16 and above.
With Node.js v14 we have to use the async_hooks
module to register callbacks tracking the lifetime of asynchronous resources. There is good documentation with an example on measuring the duration of async operations.
Completion of measurement
It’s recommended to disconnect the performance observer from all incoming notifications, when you are done with your measurements:
observer.disconnect();
Enter fullscreen mode
Exit fullscreen mode
Inclusion in Unit Tests
If you want to ensure the execution speed of your functions in the long run, you can make them part of your unit tests. Many testing frameworks (like Jest, Jasmine, and others) allow you to set a timeout for the execution of your test. The timeout feature can be used to mark a test as failed if the tested function takes too long to execute.
Here is a timeout example with the Jasmine testing framework:
businessLogic.ts
export function businessLogic(): Promise<number> {
return new Promise((resolve) => {
setTimeout(resolve, 2000);
});
}
Enter fullscreen mode
Exit fullscreen mode
businessLogic.test.ts
import {businessLogic} from './businessLogic';
describe('businessLogic', () => {
it('does not take longer than 3 seconds (3000ms)', async () => {
await businessLogic();
}, 3000);
});
Enter fullscreen mode
Exit fullscreen mode
Get connected 🔗
Please follow me on Twitter or subscribe to my YouTube channel if you liked this post. I would love to hear from you what you are building. 🙂 Best, Benny
Сегодня мы рассмотрим, сколько именно времени уходит у блока кода, чтоб выполнить свою работу?
В этом уроке вы узнаете два наиболее классических способа по измерению времени работы вашего скрипта. Данные подходы помогут вам ускорить работу вашего приложения и найти те участки кода, которые работают медленнее, чтоб оптимизировать их.
Итак, первый способ, это использование специального метода в объекте console. Все знакомы с методом console.log(), с помощью которого можно выводить сообщения и смотреть отладочную информацию и ошибки. Кроме console.log() у объекта console существует метод time со специальным синтаксисом, который позволяет буквально в две строки замерять время выполнения блока вашего кода.
Для того, чтоб этот метод заработал, требуется в начале блока, который вы хотите непосредственно замерять, указать console.time() с ключом, как уникальным идентификатором, в нашем случае это будет FirstWay.
console.time('FirstWay');
Далее идёт ваш блок кода, в нашем случае, это небольшая функция someFunction(). Когда ваш код уже выполнен и необходимо посмотреть на результат, сколько времени заняло его выполнение, вызывается специальный метод console.timeEnd() с тем же самым ключом, который вы задавали ранее.
function someFunction(){ //Ждём несколько секунд alert("Waiting...") } console.time('FirstWay'); someFunction(); console.timeEnd('FirstWay');
Таким образом, мы устанавливаем начало и конец замера времени выполнения работы кода. Результат такого замера мы сможем увидеть в консоли. Данные выводятся в миллисекундах.
Второй вариант считается более классическим подходом. В этом варианте больше строчек кода, но потребность в консоли отпадает. В этом методе требуется создать константу start. В ней мы объявляем объект new Date() с методом getTime() для установления начала отсчёта замера.
const start = new Date().getTime();
Затем следует код, который вас интересует. Очевидно, что это может быть не только функция, но и блок кода, время работы которого вы хотите замерять.
Когда скрипт выполнился, нужно создать новую константу end с объектом new Date() и методом getTime(), чтоб установить конец замера времени работы кода.
const end = new Date().getTime();
Далее, простым отниманием end — start мы и получим нужную разницу в миллисекундах и теперь нам известно, сколько времени ушло на выполнение кода.
const start= new Date().getTime(); someFunction(); const end = new Date().getTime(); console.log('SecondWay: ${end - start}ms');
Теперь вы знаете два наиболее простых и распространённых способа по измерению времени работы вашего кода! Первый способ подойдёт для тех, кто проверяет код в консоли лично, дебажить его. Второй способ подойдёт тем, кому неудобно выводить значения в консоль или же нужно использовать полученные данные в последующей работе.
Events and Timers in Node.js
Node.js has multiple utilities for handling events as well as scheduling the execution of code. These utilities, combined, give you the ability to reactively respond at the right time, for example:
- Clearing session data when a user logs out
- Scheduling a timeout for receiving results from an API call and specifying the error handling code to run in case of timeout
- Closing database connections before exiting Node.js
In this article, we will go over how timers work in Node.js. We will also introduce how the Node.js event loop works and how you can take advantage of Node’s event handling capabilities.
Timers
The first set of utilities we will look at are the setTimeout
, setImmediate
, and setInterval
timing utilities. With these tools, we can control the timing of code execution.
Why would this be important? In Node.js, similar to when using other programming languages such as C, Python, Java, and others, it helps to schedule certain functions to run repeatedly.
Suppose, for example, we want to copy certain files from a receiving location to a permanent archive. This would be a good scenario for scheduling a file transfer. At certain intervals, we can check for new files, and then copy them over to the backup location if there are any.
setTimeout
With setTimeout
, we can schedule code to be executed after a certain length of time has passed.
// setTimeout.js
let cue = 'The actors are here!';
// However, the cue is not announced until at least 5000ms have
// passed through the use of setTimeout
setTimeout(function() {
return console.log(cue);
}, 5000);
// This console log is executed right away
console.log('An exploration of art and music. And now, as we wait for the actors...');
To execute this code and see it in action, run node setTimeout.js
at your terminal:
$ node setTimeout.js
An exploration of art and music. And now, as we wait for the actors...
The actors are here!
Notice how even though the console('An exploration...')
call is after our console.log(cue)
call, it is still executed first.
The trick to realize here is that the code is only guaranteed to execute after at least that length of time has passed, not right on the dot.
setInterval
In situations where you need repeated, regular, code execution, such as long polling, then setInterval
method will be a more natural fit than setTimeout
. With this function, we can specify a function to be executed every X seconds. The function actually takes its argument in milliseconds, so you have to do the conversion yourself before entering in your arguments.
Suppose we want to check the length of the queue at a McDonald’s drive-through so users of our program can dash out at the best time. Using setInterval
, we can repeatedly check the length of the queue and tell them when the coast is clear.
// setInterval.js
// This function simulates us checking the length
// of a McDonald's drive-through queue
let getQueueLength = function() {
return Math.round(12 * Math.random());
};
// We would like to retrieve the queue length at regular intervals
// this way, we can decide when to make a quick dash over
// at the optimal time
setInterval(function() {
let queueLength = getQueueLength();
console.log(`The queue at the McDonald's drive-through is now ${queueLength} cars long.`);
if (queueLength === 0) {
console.log('Quick, grab your coat!');
}
if (queueLength > 8) {
return console.log('This is beginning to look impossible!');
}
}, 3000);
You can see the output below. Run the code with node setInterval.js
, as shown below:.
$ node setTimeout.js
The queue at the McDonald's drive-through is now 6 cars long.
The queue at the McDonald's drive-through is now 0 cars long.
Quick, grab your coat!
The queue at the McDonald's drive-through is now 1 cars long.
The queue at the McDonald's drive-through is now 3 cars long.
The queue at the McDonald's drive-through is now 9 cars long.
This is beginning to look impossible!
The queue at the McDonald's drive-through is now 0 cars long.
Quick, grab your coat!
The queue at the McDonald's drive-through is now 10 cars long.
This is beginning to look impossible!
setImmediate
If we’d like a function to be executed as urgently as possible, we use setImmediate
. The function we execute this way will execute ahead of all setTimeout
or setInterval
calls as soon as the current Node.js event loop has finished calling event callbacks.
Here’s an example of this in process. You can run this code with the command node setImmediate.js
// setImmediate.js
// A timeout
setTimeout(function() {
console.log('I am a timeout');
}, 5000);
// An interval
setInterval(function() {
console.log('I am an interval');
}, 5000);
// An immediate, its callback will be executed before those defined above
setImmediate(function() {
console.log('I am an immediate');
});
// IO callbacks and code in the normal event loop runs before the timers
console.log('I am a normal statement in the event loop, guess what comes next?');
$ node setImmediate.js
I am a normal statement in the event loop, guess what comes next?
I am an immediate
I am a timeout
I am an interval
I am an interval
I am an interval
...
The setImmediate
callback, though defined after those for setInterval
and setTimeout
, will run ahead of them.
The Event Loop
A question that might have occurred to you is «How does Node.js keep track of all these times, timers, and events? How is the order of execution prioritized?» This is a good line of inquiry and necessitates looking at something known as the «Node.js Event Loop».
So, what is the Event Loop?
The Event Loop is simply a repeated cycle by which Node.js switches through processing of computations. Since it cannot conduct all possible computations simultaneously, being single-threaded, it switches from computation to computation in a well-defined loop known as the Event Loop.
The Event Loop has the following basic stages:
- Timers — executes callbacks that have been scheduled with
setTimeout
andsetInterval
- Pending callbacks — executes any callbacks that are ready to run
- Idle, prepare — internal to Node.js
- Poll — accepts incoming connections and data processing
- Check — invokes callbacks set using
setImmediate
- Close callbacks — runs callbacks for close events
The Event Loop is the backbone of working with events and other asynchronous callbacks in Node.js. It allows us to lay hooks at certain points that will be hit in the course of the loop.
Responding to Asynchronous Returns with Callbacks
Given the single-threaded nature of Node.js, long-running operations such as file reads or database queries are quickly offloaded to the operating system, then Node.js continues its Event Loop as normal. This keeps things efficient and fast.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
How do those operating system processes interact with the Node.js process? Through the means of callbacks. We use a callback to asynchronously process things in the background, then hook back into the Event Loop once the asynchronous job has completed. To get this kind of functionality in other programming languages, you might use a task queue like Celery in Python or Sidekiq in Ruby. In Node.js, because the Event Loop and the asynchronous execution of Node.js already queue things up for you automatically, you get this asynchronous processing for free.
To see callbacks in action, we are going to read a file from the filesystem and use a call back to print out the contents.
The first step is to create the file. In this case, we are using a text file, containing the lines to a poem by T.S. Eliot. You can substitute your own file. This file is named poem.txt
and you can place the following contents into it.
// poem.txt
Macavity - The Mystery Cat, by T. S. Eliot
Macavity's a Mystery Cat: he's called the Hidden Paw--
For he's the master criminal who can defy the Law.
He's the bafflement of Scotland Yard, the Flying Squad's despair:
For when they reach the scene of crime--Macavity's not there!
Macavity, Macavity, there's no on like Macavity,
He's broken every human law, he breaks the law of gravity.
His powers of levitation would make a fakir stare,
And when you reach the scene of crime--Macavity's not there!
You may seek him in the basement, you may look up in the air--
But I tell you once and once again, Macavity's not there!
In the same directory, we will create our script that will read this poem file and print it back out. Printing the file or handling an error will be accomplished for us in a callback, after the operating system returns the result of the file read. As shown below, in readFile.js
, your callback gets triggered after the asynchronous operating system process returns. When this OS process returns, the Node.js callback you provided is placed on the event loop to be processed, which will then get executed when the loop gets to that process.
Your callback can then do anything from updating state in the application, to handling an error, if any, and logging the user out, doing nothing, or even terminating the Node process entirely.
// readFile.js
const fs = require('fs');
// Attempt to read the poem file
// Attach a callback to handle a successful read and print the contents to console
fs.readFile('./poem.txt', 'utf-8', function(err, data) {
if (err) return console.error(err);
let poem = data.toString();
console.log('Here is the poem of the day...nn');
return console.log(data);
});
Run this code with node readFile.js
. The file will be read and the console should print the poem back to you. If not, it will print out the error that was encountered, for example, if there is no such file at the specified path.
Callbacks are suitable for one-time handling of data, errors, and events. However, callbacks can get complicated when they are nested several levels deep. Another alternative way of handling events is to use Event Listeners, which are covered in the next section.
Responding to Events with Event Listeners
Event listeners are functions that get to run when specific event types occur. For example, when reading a file, making a server connection or querying a database, the modules that we make use of, such as fs
, net
, or mongoose
, all have built-in event types that they will emit.
The objects that typically emit these events extend the base EventEmitter
object, which comes from the built-in events module.
Your application can respond to these events through the mechanism of event listeners. Typically, you attach an event listener in code through the means of the keyword «on», followed by a string specifying the event type, and then finally a function, which is the code to run when the event occurs.
To see event listeners in action, we are going to create a server that interacts with a Cat API and parse responses from the API. Our server will then serve requests and show visitors a «Cat of the Day» image. The events we will be working with are part of the http
module.
We will also be using a xml2js module to parse the XML responses that the Cat API produces. To install xml2js
, you will want to run the command npm install xml2js
in a suitable project directory.
Once you have installed the module, create two files in the directory, cats.html
, and cats.js
. Inside cats.html
, place the front-end of our application. This will simply display the cat data we are going to be parsing.
<!-- cats.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cats</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container-fluid">
<div class="col-md-8 col-md-offset-2">
<h1>Cats Of Silicon Valley</h1>
<h2>Welcome to the Cat Of The Day</h2>
<img src=IMGSRC class="img-fluid" alt="Responsive image">
<br>
<label class="primary">Source: SOURCE</label>
<br>
<a href="/" class="btn btn-primary btn-lg">More Cats!</a>
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>
The bulk of our logic is going to be in the server-side code that works with the event listeners. This is contained in the cats.js
file. To see the event listener code in action, place the following code inside the file, then run it with node cats.js
, and, in your browser, visit http://localhost:4000
.
// cat.js
const http = require('http');
const fs = require('fs');
const xml2js = require('xml2js');
// We will get images from the CatAPI https://thecatapi.com/
let catApi = 'http://thecatapi.com/api/images/get?format=xml&results_per_page=1';
let catUrl = '';
let catSource = '';
let server = http.createServer(function(req, res) {
// Get fresh cat data from the Cat API
http.get(catApi, (res) => {
let data = '';
// Attach event listener for when receiving data from the remote server is complete
res.on('end', () => {
console.log('***We have completed cat datan***');
console.log(data);
let parser = new xml2js.Parser();
return parser.parseString(data, function(err, imgxml) {
if (err) {
return console.log('Error parsing cat data');
} else {
let imgjson = JSON.parse(JSON.stringify(imgxml));
console.log('***We have cat JSON***');
console.log(imgjson);
catUrl = imgjson.response.data[0].images[0].image[0].url[0];
return catSource = imgjson.response.data[0].images[0].image[0].source_url[0];
}
});
});
// Event listener for the 'data' event
// In this case, accumulate all the data so we can use it all at once later
return res.on('data', (xml) => {
return data += xml;
});
});
// Serve cat images from the CatAPI
return fs.readFile('./cats.html', function(err, cathtml) {
if (err) {
console.error(err);
return res.end('An error occurred');
}
let html = cathtml.toString()
.replace('IMGSRC', catUrl)
.replace('SOURCE', catSource);
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.write(html);
return res.end();
});
});
// Run the server
server.listen(4000);
Below, we go into the code in detail. Also take a look at the comments in the code.
As you can see from the code, our request to the Cat API requests new cat data. We then let Node.js execution continue as normal. However, we attach two event listeners to deal with new events from the remote API. The first of these is an «on end» event listener. When we have a complete cat payload from the Cat API, we then update our page with the new data and image. The second class of event we are listening for is the «data» event. This is triggered when there is new data from the remote host. In that case, we buffer up the data and add it to our temporary data store.
Now, thanks to the power of event listeners, we’ve made it easy to get new cat images at will.
Our website visitors can get new Cat of the Day images at the click of a button.
There’s a lot more to events and timers in Node.js than what we described here. A good next topic to explore is that of event emitters, which give you even more power over the kinds of events your application can make use of.