diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..01b2223a --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" +gem "ruby-prof" +gem "rspec-benchmark" +gem "minitest" +gem "memory_profiler" +gem "stackprof" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..0ff4d07f --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,43 @@ +GEM + remote: https://rubygems.org/ + specs: + benchmark-malloc (0.2.0) + benchmark-perf (0.6.0) + benchmark-trend (0.4.0) + diff-lcs (1.6.0) + memory_profiler (1.1.0) + minitest (5.25.4) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-benchmark (0.6.0) + benchmark-malloc (~> 0.2) + benchmark-perf (~> 0.6) + benchmark-trend (~> 0.4) + rspec (>= 3.0) + rspec-core (3.13.3) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + ruby-prof (1.6.3) + stackprof (0.2.27) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + memory_profiler + minitest + rspec-benchmark + ruby-prof + stackprof + +BUNDLED WITH + 2.4.10 diff --git a/case-study-template.md b/case-study-template.md deleted file mode 100644 index c3279664..00000000 --- a/case-study-template.md +++ /dev/null @@ -1,55 +0,0 @@ -# Case-study оптимизации - -## Актуальная проблема -В нашем проекте возникла серьёзная проблема. - -Необходимо было обработать файл с данными, чуть больше ста мегабайт. - -У нас уже была программа на `ruby`, которая умела делать нужную обработку. - -Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. - -Я решил исправить эту проблему, оптимизировав эту программу. - -## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* - -## Гарантия корректности работы оптимизированной программы -Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. - -## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* - -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* - -## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* - -Вот какие проблемы удалось найти и решить - -### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -## Результаты -В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. - -*Какими ещё результами можете поделиться* - -## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..32124cdd --- /dev/null +++ b/case-study.md @@ -0,0 +1,113 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: +* время выполнения программы. +* потребление памяти + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 1-2 минуты + +Вот как я построил `feedback_loop`: подготовил файлы с различным количеством строк (head -n N data_large.txt > dataN.txt), чтобы программа могла выполняться за 10–20 секунд. +После каждого изменения я запускал программу на файлах с разным количеством строк и смотрел на результаты отчетов. + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался: +- gem memory_profiler +- gem ruby-prof и отчеты callstack & qcachegrind +- gem stackprof + CLI и Speedscope +- второй thread для мониторинга памяти + +Вот какие проблемы удалось найти и решить: + +### №1 Уменьшение создания временных массивов при добавлении элементов +``` + sessions = sessions + [parse_session(line)] if cols[0] == 'session' +``` +- memory_profiler +- Вместо оператора + было применено использование метода <<, который добавляет элемент в существующий массив без создания нового объекта. +- До оптимизации программе аллоцировалось 460MB памяти на файле размером 10_000 строк, после оптимизации уже 155MB +- данная проблема перестала быть главной точкой роста + +### №2 Избыточное создание массивов при фильтрации сессий для каждого пользователя +``` + user_sessions = sessions.select { |session| session['user_id'] == user['id'] } +``` +- memory_profiler +- Проблема возникает в строке user_sessions = sessions.select { |session| session['user_id'] == user['id'] }, где для каждого пользователя создается новый массив отфильтрованных сессий. Это приводит к повторным обходам большого массива sessions, и для каждого пользователя в памяти хранятся временные массивы, что заметно увеличивает использование памяти. +- Чтобы избежать повторного обхода массива для каждого пользователя и избыточного создания временных массивов, все сессии были предварительно сгруппированы по user_id с использованием метода group_by. После этого для каждого пользователя мы просто обращаемся к уже сгруппированным данным через хеш (sessions_by_user[user['id']]). +- До оптимизации программе аллоцировалось 155MB памяти на файле размером 10_000 строк, после оптимизации уже 42MB. +- данная проблема перестала быть главной точкой роста + +### №3 Уменьшение создания временных массивов при добавлении элементов +``` + users = users + [parse_user(line)] if cols[0] == 'user' +``` +- memory_profiler +- Вместо оператора + было применено использование метода <<, который добавляет элемент в существующий массив без создания нового объекта. +- До оптимизации программе аллоцировалось 636MB памяти на файле размером 50_000 строк, после оптимизации уже 400MB +- данная проблема перестала быть главной точкой роста + +### №4 Уменьшение создания временных массивов при добавлении элементов +``` + users_objects = users_objects + [user_object] +``` +- memory_profiler +- Вместо оператора + было применено использование метода <<, который добавляет элемент в существующий массив без создания нового объекта. +- До оптимизации программе аллоцировалось 400MB памяти на файле размером 50_000 строк, после оптимизации уже 160MB +- Данная проблема перестала быть главной точкой роста + +### №5 Излишний парсинг дат и преобразование в формат iso8601 +``` + { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } +``` +- qcachegrind из ruby-prof +- Так как мы уже имеем данные в нужном формате, то было принято решение не тратить время на преобразование даты в формат iso8601 +- До оптимизации программе аллоцировалось 160MB памяти на файле размером 50_000 строк, после оптимизации уже 120MB +- Данная проблема перестала быть главной точкой роста + +### №6 Избыточное создание временных массивов +``` +cols = line.split(',') +``` +- memory_profiler +- Вместо разделения строки на части с помощью split и проверки первого элемента, я решил использовать метод start_with? +- До оптимизации программе аллоцировалось 120MB памяти на файле размером 50_000 строк, после оптимизации уже 100MB. +- Данная проблема перестала быть главной точкой роста + +### №7 Избыточное потребление памяти из-за создания новых хэшей при merge +``` + report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) +``` +- memory_profiler +- Был использован метод merge! вместо merge, который изменяет оригинальный хэш на месте, избегая лишнего копирования данных +- До оптимизации программе аллоцировалось 200MB памяти на файле размером 100_000 строк, после оптимизации уже 184MB. +- Данная проблема перестала быть главной точкой роста + +### №8 Вспомнил про волшебный коммент `# frozen_string_literal: true` +- Видеоурок +- Добавил в начало файла `# frozen_string_literal: true` +- До оптимизации программе аллоцировалось 184MB памяти и 2.5 млн объектов на файле размером 100_000 строк, после оптимизации уже 166MB и только 2млн объектов. + +Дальнейшие оптимизации не приносили существенного прироста по эффективности. Было принято решение переписать приложение на потоковую обработку файла. + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось добиться стабильного потребления памяти в пределах 20-30 мб при обработке файлов любых размеров. + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы был добавлен тест производительности performance_test.rb diff --git a/memory_watcher.rb b/memory_watcher.rb new file mode 100644 index 00000000..fa1b87f9 --- /dev/null +++ b/memory_watcher.rb @@ -0,0 +1,26 @@ +class MemoryWatcher + def initialize(memory_limit_mb) + @memory_limit_mb = memory_limit_mb + @should_stop = false + end + + def start + @thread = Thread.new do + until @should_stop + current_memory = `ps -o rss= -p #{Process.pid}`.to_i / 1024 + puts "MEMORY USAGE: #{current_memory} MB" + if current_memory > @memory_limit_mb + puts "Memory limit exceeded: #{current_memory}MB > #{@memory_limit_mb}MB" + puts "Killing process..." + Process.kill('KILL', Process.pid) + end + sleep 1 + end + end + end + + def stop + @should_stop = true + @thread.join if @thread + end +end diff --git a/performance_test.rb b/performance_test.rb new file mode 100644 index 00000000..16d38acc --- /dev/null +++ b/performance_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rspec' +require_relative 'task-2' + +RSpec.describe 'Memory usage' do + before { File.write('result.json', '') } + + it 'consumes no more than 70MB of memory' do + memory_before = `ps -o rss= -p #{Process.pid}`.to_i / 1024 + + work('data_large.txt', true) + + memory_after = `ps -o rss= -p #{Process.pid}`.to_i / 1024 + memory_usage = memory_after - memory_before + + puts "Memory usage during test: #{memory_usage} MB" + expect(memory_usage).to be <= 70 + end +end + diff --git a/task-2.rb b/task-2.rb index 34e09a3c..148e8eba 100644 --- a/task-2.rb +++ b/task-2.rb @@ -1,177 +1,95 @@ -# Deoptimized version of homework task - -require 'json' -require 'pry' -require 'date' -require 'minitest/autorun' - -class User - attr_reader :attributes, :sessions - - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end -end - -def parse_user(user) - fields = user.split(',') - parsed_result = { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], - } -end - -def parse_session(session) - fields = session.split(',') - parsed_result = { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5], - } -end - -def collect_stats_from_users(report, users_objects, &block) - users_objects.each do |user| - user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" - report['usersStats'][user_key] ||= {} - report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) - end -end - -def work - file_lines = File.read('data.txt').split("\n") - - users = [] - sessions = [] - - file_lines.each do |line| - cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' - end - - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + - - report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end - - report['uniqueBrowsersCount'] = uniqueBrowsers.count - - report['totalSessions'] = sessions.count - - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') - - # Статистика по пользователям - users_objects = [] - - users.each do |user| - attributes = user - user_sessions = sessions.select { |session| session['user_id'] == user['id'] } - user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] - end - - report['usersStats'] = {} - - # Собираем количество сессий по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } - end - - # Собираем количество времени по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } - end - - # Выбираем самую длинную сессию пользователя - collect_stats_from_users(report, users_objects) do |user| - { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } - end - - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end - - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end - - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 - collect_stats_from_users(report, users_objects) do |user| - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } - end - - File.write('result.json', "#{report.to_json}\n") - puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +#frozen_string_literal: true + +require_relative 'memory_watcher' + +def write_sessions(file, user_sessions_count, user_total_time, user_longest_session, user_browsers, user_dates, ie, chrome) + file.write <<-JSON + \"sessionsCount\": #{user_sessions_count}, + \"totalTime\": "#{user_total_time} min.", + \"longestSession\": "#{user_longest_session} min.", + \"browsers\": "#{user_browsers.sort.join(', ')}", + \"usedIE\": #{ie}, + \"alwaysUsedChrome\": #{chrome}, + \"dates\": #{user_dates.sort.reverse} + } + JSON end -class TestMe < Minitest::Test - def setup - File.write('result.json', '') - File.write('data.txt', -'user,0,Leida,Cira,0 -session,0,0,Safari 29,87,2016-10-23 -session,0,1,Firefox 12,118,2017-02-27 -session,0,2,Internet Explorer 28,31,2017-03-28 -session,0,3,Internet Explorer 28,109,2016-09-15 -session,0,4,Safari 39,104,2017-09-27 -session,0,5,Internet Explorer 35,6,2016-09-01 -user,1,Palmer,Katrina,65 -session,1,0,Safari 17,12,2016-10-21 -session,1,1,Firefox 32,3,2016-12-20 -session,1,2,Chrome 6,59,2016-11-11 -session,1,3,Internet Explorer 10,28,2017-04-29 -session,1,4,Chrome 13,116,2016-12-28 -user,2,Gregory,Santos,86 -session,2,0,Chrome 35,6,2018-09-21 -session,2,1,Safari 49,85,2017-05-22 -session,2,2,Firefox 47,17,2018-02-02 -session,2,3,Chrome 20,84,2016-11-25 -') +def work(file_path = 'data.txt', memory_watcher = false) + if memory_watcher + memory_watcher = MemoryWatcher.new(70) + memory_watcher.start end - def test_result - work - expected_result = JSON.parse('{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}') - assert_equal expected_result, JSON.parse(File.read('result.json')) + all_browsers = Set.new + total_users = 0 + total_sessions = 0 + + first_user = true + user_sessions_count = 0 + user_total_time = 0 + user_longest_session = 0 + user_browsers = [] + ie = false + chrome = true + user_dates = [] + + begin + File.open("result.json", 'w') do |file| + file.write("{ \"usersStats\":{") + + File.foreach(file_path, chomp: true).each do |line| + record_type, _, user_name_or_session_id, user_second_name_or_browser_name, session_time, browser_date = line.split(',') + if record_type == 'user' + unless first_user + write_sessions(file, user_sessions_count, user_total_time, user_longest_session, user_browsers, user_dates, ie, chrome) + file.write ',' + end + + file.write "\"#{user_name_or_session_id} #{user_second_name_or_browser_name}\": {" + total_users += 1 + first_user = false + user_total_time = 0 + user_longest_session = 0 + user_browsers = [] + user_dates = [] + ie = false + chrome = true + user_sessions_count = 0 + elsif record_type == "session" + user_session_time = session_time.to_i + user_total_time += user_session_time + + user_longest_session = user_session_time if user_session_time > user_longest_session + + user_browsers << user_second_name_or_browser_name.upcase! + + unless ie + ie = true if user_second_name_or_browser_name =~ /INTERNET EXPLORER/ + end + if chrome + chrome = false unless user_second_name_or_browser_name =~ /CHROME/ + end + + user_dates << browser_date + user_sessions_count += 1 + total_sessions += 1 + all_browsers.add(user_second_name_or_browser_name) + end + end + + write_sessions(file, user_sessions_count, user_total_time, user_longest_session, user_browsers, user_dates, ie, chrome) + + file.write("},") + file.write "\"uniqueBrowsersCount\": #{all_browsers.count}," + file.write "\"totalSessions\": #{total_sessions}," + file.write "\"allBrowsers\": \"#{all_browsers.sort.join(',')}\"," + file.write "\"totalUsers\": #{total_users}" + file.write("}") + end + + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + ensure + memory_watcher.stop if memory_watcher end end diff --git a/test.rb b/test.rb new file mode 100644 index 00000000..346b36af --- /dev/null +++ b/test.rb @@ -0,0 +1,35 @@ +require 'minitest/autorun' +require 'json' +require_relative 'task-2.rb' + +class TestMe < Minitest::Test + def setup + File.write('result.json', '') + File.write('data.txt', + 'user,0,Leida,Cira,0 +session,0,0,Safari 29,87,2016-10-23 +session,0,1,Firefox 12,118,2017-02-27 +session,0,2,Internet Explorer 28,31,2017-03-28 +session,0,3,Internet Explorer 28,109,2016-09-15 +session,0,4,Safari 39,104,2017-09-27 +session,0,5,Internet Explorer 35,6,2016-09-01 +user,1,Palmer,Katrina,65 +session,1,0,Safari 17,12,2016-10-21 +session,1,1,Firefox 32,3,2016-12-20 +session,1,2,Chrome 6,59,2016-11-11 +session,1,3,Internet Explorer 10,28,2017-04-29 +session,1,4,Chrome 13,116,2016-12-28 +user,2,Gregory,Santos,86 +session,2,0,Chrome 35,6,2018-09-21 +session,2,1,Safari 49,85,2017-05-22 +session,2,2,Firefox 47,17,2018-02-02 +session,2,3,Chrome 20,84,2016-11-25 +') + end + + def test_result + work + expected_result = JSON.parse('{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}') + assert_equal expected_result, JSON.parse(File.read('result.json')) + end +end