From 05db34310cbb25a1893ef0a8e9054bc4a69304ea Mon Sep 17 00:00:00 2001 From: Pavel Semenkin Date: Fri, 21 Feb 2025 23:21:31 +0400 Subject: [PATCH] task-2 add optimization --- Gemfile | 2 + Gemfile.lock | 13 ++++++ benchmark.rb | 7 ++++ case-study.md | 69 +++++++++++++++++++++++++++++++ task-2_with_file.rb | 99 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 190 insertions(+) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 benchmark.rb create mode 100644 case-study.md create mode 100644 task-2_with_file.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..4f870b38 --- /dev/null +++ b/Gemfile @@ -0,0 +1,2 @@ +source "https://rubygems.org" +gem 'memory_profiler' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..662b2788 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,13 @@ +GEM + remote: https://rubygems.org/ + specs: + memory_profiler (1.0.2) + +PLATFORMS + x86_64-darwin-20 + +DEPENDENCIES + memory_profiler + +BUNDLED WITH + 2.4.10 diff --git a/benchmark.rb b/benchmark.rb new file mode 100644 index 00000000..6d9a41f6 --- /dev/null +++ b/benchmark.rb @@ -0,0 +1,7 @@ +require 'benchmark' +require_relative 'task-2_with_file.rb' + +time = Benchmark.realtime do + work('data_large.txt') +end +puts "Finish in #{time.round(2)}" \ No newline at end of file diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..d1a00784 --- /dev/null +++ b/case-study.md @@ -0,0 +1,69 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: проверка использования памяти на файле с меньшим количество строк (40000 строк). + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: *как вы построили feedback_loop* +решил использовать файл с 40000 строк для оптимизации +и последовательно оптимизировать "точки роста" + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался гемом memory_profiler + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- memory_profiler показал, что формирование массива sessions занимает 4.59 Gb +- как вы решили её оптимизировать - решил писать напрямую в sessions без создания промежуточных массивов +- как изменилась метрика: память уменьшилась с 6.68 GB до 2.09 GB +- как изменился отчёт профилировщика - формирование массива session теперь не занимает памяти больше всего. + +### Ваша находка №2 +- memory_profiler показал, что формирование массива user_sessions занимает 1.66 GB +- как вы решили её оптимизировать - решил переделать формирование массива users_objects +- как изменилась метрика: память уменьшилась с 2.09 GB до 278.46 MB +- как изменился отчёт профилировщика - формирование массива session теперь не занимает памяти больше всего. + +### Ваша находка №3 +- memory_profiler показал, что формирование массива users занимает 152.96 MB +- как вы решили её оптимизировать - решил писать напрямую в users без создания промежуточных массивов +- как изменилась метрика: память уменьшилась с 278.46 MB до 127.15 MB MB +- как изменился отчёт профилировщика - формирование массива users теперь не занимает памяти больше всего. + +### Ваша находка №4 +- memory_profiler показал, что получение "Даты сессий через запятую в обратном порядке в формате" занимает 30.83 MB MB +- как вы решили её оптимизировать - решил упростить получение дат сессий +- как изменилась метрика: память уменьшилась с 127.15 MB MB до 98.00 MB +- как изменился отчёт профилировщика - получение "Даты сессий через запятую в обратном порядке в формате" теперь не занимает памяти больше всего. + +### Ваша находка №5 +- memory_profiler показал, что split строк файла занимает 18.95 MB +- как вы решили её оптимизировать - было решено переделать и написать в "потоковом" стиле +- как изменилась метрика: память уменьшилась с 98.00 MB до 15 MB +- как изменился отчёт профилировщика - обработка строк большого файла теперь занимает 15 MB и 27,47 секунд. + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с 4.59 Gb (на файле в 40000 строк) до 15 MB (на большом файле) и уложиться в заданный бюджет. + +*Какими ещё результами можете поделиться* + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* diff --git a/task-2_with_file.rb b/task-2_with_file.rb new file mode 100644 index 00000000..42dce724 --- /dev/null +++ b/task-2_with_file.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'json' +require 'oj' +require 'set' + +class User + attr_accessor :session_stats, :sessions, :first_name, :last_name + + def initialize(id, first_name, last_name, age) + @id = id + @first_name = first_name + @last_name = last_name + @age = age + @sessions = [] + @session_stats = {} + end +end + +def parse_session(fields) + { + 'user_id' => fields[0], + 'session_id' => fields[1], + 'browser' => fields[2], + 'time' => fields[3], + 'date' => fields[4] + } +end + +def collect_stats_from_user(user) + return {} unless user + + stats = { + 'sessionsCount' => user.sessions.count, + 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.', + 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.', + 'browsers' => user.sessions.map {|s| s['browser']}, + 'dates' => user.sessions.map { |s| s['date'] }.sort.reverse + } + + stats['usedIE'] = stats['browsers'].any? { |b| b =~ /INTERNET EXPLORER/ } + stats['alwaysUsedChrome'] = stats['browsers'].all? { |b| b =~ /CHROME/ } + stats['browsers'] = stats['browsers'].sort.join(', ') + stats['dates'].sort!.reverse! + stats +end + +def write_user(user, stream_writer) + stream_writer.push_key("#{user.first_name} #{user.last_name}") + stream_writer.push_object + user.session_stats.each { |key, value| stream_writer.push_value(value, key.to_s) } + stream_writer.pop +end + +def work(file_name) + total_users = 0 + total_sessions = 0 + unique_browsers = Set.new + user = nil + + result_file = File.open('result.json', 'w') + + stream_writer = Oj::StreamWriter.new(result_file) + stream_writer.push_object + stream_writer.push_key('usersStats') + stream_writer.push_object + + File.foreach(file_name) do |line| + type, *info = line.strip!.split(',') + if type == 'user' + total_users += 1 + user.session_stats = collect_stats_from_user(user) if user + write_user(user, stream_writer) if user + user = User.new(*info) + end + + if type == 'session' + total_sessions += 1 + session = parse_session(info) + user.sessions << session + unique_browsers << session['browser'].upcase! + end + end + + user.session_stats = collect_stats_from_user(user) if user + write_user(user, stream_writer) if user + + stream_writer.pop + + stream_writer.push_value(total_users, 'totalUsers') + stream_writer.push_value(unique_browsers.count, 'uniqueBrowsersCount') + stream_writer.push_value(total_sessions, 'totalSessions') + stream_writer.push_value(unique_browsers.sort.join(','), 'allBrowsers') + + stream_writer.pop_all + result_file.close + + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) +end \ No newline at end of file