Я продолжаю погружение в Эрланг. Уже есть хитрый план переписать один из наших сервисов для мониторинга на Эрланге. Мы тут осваиваем облака Windows Azure и Amazon EC2 в качестве платформы для некоторых продуктов и внутренних задач типа QA, поэтому возможность использовать много ядер и машин без переписывания кода выглядить перспективно.
Итак, для начала простой, но реальный пример - есть проект ~2000 файлов. Надо составить список используемых переменных окружения. То есть найти вхождения строк "getenv(...)" и "GetVariable(...)" (это наш wrapper) и выдрать из них параметр.
Задача незамысловатая и давно решается программой на C++, которая даже обход каталогов не делает, а просто вызывает юниксовый "find", генерирующий список файлов по маске, и затем по списку лопатит файлы. На 2000 файлах работает пару секунд в один поток.
Теперь Эрланг. Тут хочется замутить что-нибудь более кучерявое, чем последовательный обход файлов. MapReduce как раз в тему - можно составить список файлов, затем анализ каждого файла делать параллельно (Map), аккумулируя найденные имена переменных, и в конце обработать все полученные входждение (Reduce), в нашем случае просто подсчитать количество вхождения каждой переменной.
Фактически мой код повторяет пример из "Programming Erlang" и использует модуль phofs (parallel higher-order functions) из этой же книги.
-module(find_variables).
-export([main/0, find_variables_in_file/2, process_found_variables/3]).
-define(PATH, "/Projects/interesting_project").
-define(MASK, "\\..*(cpp|c)").
main() ->
io:format("Creating list of files...~n", []),
% Стандартная функция обхода файловой системы. Последний параметр -
% функтор, накапливающий имена в списке.
Files = filelib:fold_files(?PATH, ?MASK, true,
fun(N, A) -> [N | A] end, []),
io:format("Found ~b file(s)~n", [length(Files)]),
F1 = fun find_variables_in_file/2, % Map
F2 = fun process_found_variables/3, % Reduce
% Вызываем MapReduce через функцию benchmark, считающую время
% выполнения.
benchmark(fun() ->
L = phofs:mapreduce(F1, F2, [], Files),
io:format("Found ~b variable(s)~n", [length(L)])
end, "MapReduce").
benchmark(Worker, Title) ->
{T, _} = timer:tc(fun() -> Worker() end),
io:format("~s: ~f sec(s)~n", [Title, T/1000000]).
-define(REGEXP, "(getenv|GetVariable)\s*\\(\s*\"([^\"]+)\"\s*\\)").
% Map. Анализ одного файла.
find_variables_in_file(Pid, FileName) ->
case file:open(FileName, [read]) of
{ok, File} ->
% Заранее компилируем регулярное выражение.
{_, RE} = re:compile(?REGEXP),
% Данный обратный вызов пошлет родительскому контролирующему
% потому сообщение с именем найденной переменной.
CallBack = fun(Var) -> Pid ! {Var, 1} end,
find_variable_in_file(File, RE, CallBack),
file:close(File);
{error, Reason} ->
io:format("Unable to process '~s', ~p~n", [FileName, Reason]),
exit(1)
end.
% Reduce. Анализ данных. Данная функция вызывается контролирующим
% процессом MapReduce для каждого найденного ключа вместе со списком
% значений, ассоциированных с ним. В нашем случае это будут пары
% {VarName, 1}. Мы просто подсчитаем для каждого VarName количество
% пришедших пар, то есть количество найденных вхождений этой переменной.
% Это и есть наш незамысловатый анализ.
process_found_variables(Key, Vals, A) ->
[{Key, length(Vals)} | A].
% Построчный обход файла.
find_variable_in_file(File, RE, CallBack) ->
case io:get_line(File, "") of
eof -> void;
Line ->
scan_line_in_file(Line, RE, CallBack),
find_variable_in_file(File, RE, CallBack)
end.
% Поиск строки в строке по регулярному выражению (скомпилированному ранее),
% и в случае нахождение вызов CallBack с передачей ему имени найденной
% переменной.
scan_line_in_file(Line, RE, CallBack) ->
case re:run(Line, RE) of
{match, Captured} ->
[_, _, {NameP, NameL}] = Captured,
Name = string:substr(Line, NameP + 1, NameL),
CallBack(Name);
nomatch -> void
end.
Для сборки программы нужен модуль phofs. Он является универсальным, независимым от конкретных функций Map и Reduce.
И Makefile на всякий случай:
target = find_variables
all:
erlc $(target).erl
erlc phofs.erl
erl -noshell -s $(target) main -s init stop
clean:
-rm *.beam *.dump
Пузомерка. Как я уже сказал, программа на C++ вместе со временем вызова "find" на моей машине работает 1-2 секунды. Версия на Erlang'e работает ~20 секунд. Плохо? Смотря как посмотреть. Если анализ каждого файла будет более длительным (то есть программа будет основное время тратить на анализ файла, а не обход каталогов), то тут уже не совсем очевидно, какое из решений будет более практично при увеличении числа файлов и сложности анализа.
Я новичок в Эрланге, поэтому будут признателен за критику кода.
Посты по теме: