Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM ruby:2.6-stretch

RUN apt-get update && apt-get install g++ valgrind -y \
massif-visualizer \
--no-install-recommends \
&& apt-get purge --auto-remove -y curl \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /src/*.deb

RUN groupadd -r massif && useradd -r -g massif massif \
&& mkdir -p /home/massif/test && chown -R massif:massif /home/massif
USER massif
WORKDIR /home/massif/test

COPY Gemfile /home/massif/test
RUN bundle install
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem 'oj'
gem 'progress_bar'
19 changes: 19 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
GEM
remote: https://rubygems.org/
specs:
highline (2.0.3)
oj (3.13.13)
options (2.3.2)
progress_bar (1.3.3)
highline (>= 1.6, < 3)
options (~> 2.3.0)

PLATFORMS
ruby

DEPENDENCIES
oj
progress_bar

BUNDLED WITH
1.17.2
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)"
- в описание `PR` добавьте чеклист и отметьте, что из него сделали; для получения максимальной пользы надо отметить всё.

## Checklist
- [ ] Построить и проанализировать отчёт гемом `memory_profiler`
- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`;
- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`;
- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`;
- [ ] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`;
- [ ] Построить и проанализировать текстовый отчёт `stackprof`;
- [x] Построить и проанализировать отчёт гемом `memory_profiler`
- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Flat`;
- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `Graph`;
- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallStack`;
- [x] Построить и проанализировать отчёт `ruby-prof` в режиме `CallTree` c визуализацией в `QCachegrind`;
- [x] Построить и проанализировать текстовый отчёт `stackprof`;
- [ ] Построить и проанализировать отчёт `flamegraph` с помощью `stackprof` и визуализировать его в `speedscope.app`;
- [ ] Построить график потребления памяти в `valgrind massif visualier` и включить скриншот в описание вашего `PR`;
- [ ] Написать тест, на то что программа укладывается в бюджет по памяти
- [x] Построить график потребления памяти в `valgrind massif visualier` и включить скриншот в описание вашего `PR`;
- [x] Написать тест, на то что программа укладывается в бюджет по памяти

Не нужно включать в `PR` выводы всех этих отчётов, просто используйте каждый хотя бы по разу в вашем `Case-study`.

Expand Down
3 changes: 3 additions & 0 deletions build-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

docker build . -t spajic/docker-valgrind-massif
85 changes: 61 additions & 24 deletions case-study-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,81 @@
Я решил исправить эту проблему, оптимизировав эту программу.

## Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика*
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: какое количество памяти использует программа для файла размером 20000 строк. Начальное значение - около 82 МБ.

## Гарантия корректности работы оптимизированной программы
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации.

## Feedback-Loop
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось*
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за ~30 секунд

Вот как я построил `feedback_loop`: *как вы построили feedback_loop*
Вот как я построил `feedback_loop`:
1. Изучал отчеты профилировщиков
2. Вносил изменения в программу
3. Запускал тесты для проверки корректности работы программы
4. Запускал тесты производительности

## Вникаем в детали системы, чтобы найти главные точки роста
Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались*
Для того, чтобы найти "точки роста" для оптимизации я воспользовался:
1. Гемом memory_profiler;
2. Stackprof;
3. Ruby-prof в режиме graph;
4. Ruby-prof в режиме callstack;
5. Ruby-prof в режиме flat;
6. Ruby-prof с визуализацией в QCacheGrind.

Вот какие проблемы удалось найти и решить

### Ваша находка №1
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика

### Ваша находка №2
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика

### Ваша находка №X
- какой отчёт показал главную точку роста
- как вы решили её оптимизировать
- как изменилась метрика
- как изменился отчёт профилировщика
### Выделение 15мб памяти для парсинга даты
- какой отчёт показал главную точку роста: гем memory_profiler
- как вы решили её оптимизировать: убрал лишние вызовы метода map, а также переводы в различные форматы
- как изменилась метрика: выделение памяти снизилось до 24.6 Мб
- как изменился отчёт профилировщика: проблема перестала быть главной точкой роста

### Большое количество аллокаций для метода String#upcase
- какой отчёт показал главную точку роста: Stackprof
- как вы решили её оптимизировать: Всего было 4 вызова метода upcase, все они были использованы для браузеров сессий. Я оставил только 1 вызов метода upcase - при парсинге браузера сессий, тогда все остальные вызовы можно убрать
- как изменилась метрика: снизилась до 23 МБ
- как изменился отчёт профилировщика: проблема перестала быть главной точкой роста

### Выделение большого количества памяти при сборе данных о пользователе
- какой отчёт показал главную точку роста: Ruby-prof в режиме graph
- как вы решили её оптимизировать: изначально блок кода выглядел так:
```
user_key = "#{@user['first_name']}" + ' ' + "#{@user['last_name']}"
user_data = {
'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']}.sort.join(', '),
'usedIE' => @user_sessions.map{|s| s['browser']}.any? { |b| b =~ /INTERNET EXPLORER/ },
'alwaysUsedChrome' => @user_sessions.map{|s| s['browser']}.all? { |b| b =~ /CHROME/ },
'dates' => @user_sessions.map{|s| s['date']}.sort.reverse
}
```
В нем я изменил конкатенацию строк, лишние приведения значений к типу int, использовал bang-методы, где это возможно. В результате данный блок кода стал выглядеть следующим образом:

```
user_key = "#{@user['first_name']} #{@user['last_name']}"
user_data = {
'sessionsCount' => @user_sessions.count,
'totalTime' => @user_sessions.map {|s| s['time']}.sum.to_s + ' min.',
'longestSession' => @user_sessions.map {|s| s['time']}.max.to_s + ' min.',
'browsers' => @user_sessions.map {|s| s['browser']}.sort!.join(', '),
'usedIE' => @user_sessions.map{|s| s['browser']}.any? { |b| b =~ /INTERNET EXPLORER/ },
'alwaysUsedChrome' => @user_sessions.map{|s| s['browser']}.all? { |b| b =~ /CHROME/ },
'dates' => @user_sessions.map{|s| s['date']}.sort!.reverse!
}
```

- как изменилась метрика: снизилась до 18 МБ
- как изменился отчёт профилировщика: главная проблема не является главной точкой роста

## Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными.
Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет.
Удалось улучшить метрику системы с 80 МБ для файла размеров 20000 строк до 40 МБ на протяжении всей работы программы для основного файла и уложиться в заданный бюджет.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут мы как-то пропустили шаг с переписыванием в потоковый вариант


*Какими ещё результами можете поделиться*
Также хочу отметить, что программа сейчас работает быстрее, чем программа из первого домашнего задания. Удалось добиться результата в 27 секунд.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы performance-тестах, которые вы написали*
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы я написал performance-test, который проверяет, что программа потребляет менее 40 МБ памяти для файла data_large.txt
40 changes: 40 additions & 0 deletions json_writer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'oj'

class JsonWriter
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

очень чистенько получилось

attr_reader :file_writer

def initialize(file_name)
@file_writer = Oj::StreamWriter.new(file_name)
end

def prepare_user_stats
@file_writer.push_object
@file_writer.push_key('usersStats')
@file_writer.push_object
end

def write_user_stats(user_key, user_data)
@file_writer.push_key(user_key)
@file_writer.push_value(user_data)
end

def write_common_stats(users_count, unique_browsers, sessions_count)
@file_writer.pop

@file_writer.push_key('totalUsers')
@file_writer.push_value(users_count)

@file_writer.push_key('uniqueBrowsersCount')
@file_writer.push_value(unique_browsers.count)

@file_writer.push_key('totalSessions')
@file_writer.push_value(sessions_count)

@file_writer.push_key('allBrowsers')
@file_writer.push_value(unique_browsers.sort.join(','))

@file_writer.pop

@file_writer.flush
end
end
Binary file added massif_vizualizer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions memory_profiler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require_relative 'parser'
require 'benchmark'
require 'memory_profiler'

report = MemoryProfiler.report do
Parser.new.work(file_name: 'data20000.txt')
end

report.pretty_print(scale_bytes: true)
21 changes: 21 additions & 0 deletions memory_usage_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'rspec-benchmark'
require 'rspec'
require 'bytesize'

require_relative 'parser'

RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
end

RSpec.describe 'parser' do
describe 'memory usage' do
let(:expected_memory_usage) { ByteSize.mb(40).to_bytes }

it 'uses less than expected memory' do
expect {
Parser.new.work(file_name: 'data_large.txt')
}.to perform_allocation(expected_memory_usage).bytes
end
end
end
93 changes: 93 additions & 0 deletions parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

require_relative 'json_writer'

class Parser
attr_accessor :users_count, :sessions_count, :user, :user_sessions, :json_writer
attr_reader :unique_browsers

def initialize
@users_count = 0
@unique_browsers = []
@sessions_count = 0
end

def work(file_name: "data.txt", disable_gc: false)
GC.disable if disable_gc
file_path = ENV['DATA_FILE'] || file_name

File.open('result.json', 'a') do |output_file|
@json_writer = JsonWriter.new(output_file)
@json_writer.prepare_user_stats

File.foreach(file_path, 'user') do |chunk|
chunk.split("\n") do |line|
cols = line.split(',')
case cols[0]
when 'user'
next
when ''
parse_user(cols)
when 'session'
parse_session(cols)
end
end
end

collect_stats_for_previous_user

@json_writer.write_common_stats(@users_count, @unique_browsers, @sessions_count)

puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024)
end
end

private

def collect_unique_browsers
@user_sessions.each do |session|
browser = session['browser']
@unique_browsers.push(browser) unless @unique_browsers.include?(browser)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Set instead

end
end

def parse_user(fields)
collect_stats_for_previous_user if @users_count > 0
@users_count += 1

@user_sessions = []
@user = {
'first_name' => fields[2],
'last_name' => fields[3],
}
end

def parse_session(fields)
@sessions_count += 1

@user_sessions.push(
{
'browser' => fields[3].upcase!,
'time' => fields[4].to_i,
'date' => fields[5],
}
)
end

def collect_stats_for_previous_user
collect_unique_browsers

user_key = "#{@user['first_name']} #{@user['last_name']}"
user_data = {
'sessionsCount' => @user_sessions.count,
'totalTime' => @user_sessions.map {|s| s['time']}.sum.to_s + ' min.',
'longestSession' => @user_sessions.map {|s| s['time']}.max.to_s + ' min.',
'browsers' => @user_sessions.map {|s| s['browser']}.sort!.join(', '),
'usedIE' => @user_sessions.map{|s| s['browser']}.any? { |b| b =~ /INTERNET EXPLORER/ },
'alwaysUsedChrome' => @user_sessions.map{|s| s['browser']}.all? { |b| b =~ /CHROME/ },
'dates' => @user_sessions.map{|s| s['date']}.sort!.reverse!
}

@json_writer.write_user_stats(user_key, user_data)
end
end
7 changes: 7 additions & 0 deletions profile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

docker run -it \
-v $(pwd):/home/massif/test \
-e DATA_FILE=data_large.txt \
spajic/docker-valgrind-massif \
valgrind --tool=massif ruby work.rb
Loading