Статьи

Почему Rubygems Slow?

rubygemsslow

«Почему именно Rubygems работает медленно?» — это вопрос, который задавали более одного разработчика, но немногие удосужились что-либо предпринять. Недавно @mfazekas взял на себя задачу по профилированию особенно медленного случая с использованием dtrace . Это привело к нескольким высокопрофильным запросам на извлечение данных для повышения производительности и уменьшения выделения памяти. Это, в свою очередь, заставляет меня задать вопрос, а что же делает Rubygems, что занимает так много времени? Короткий ответ: намного больше, чем вы когда-либо думали; для длинного ответа продолжайте читать.

Основы Rubygems

Драгоценный камень Rubygems поставляется с современными версиями Ruby. Его цель — упростить установку и использование внешних фрагментов кода (или библиотек). Для этого он принимает Kernel#require чтобы после запуска:

 $ gem install wicked 

Позже вы можете require 'wicked' и правильный код будет загружен.

Путь загрузки и разрешения

Когда вам require wicked вы не указываете версию для загрузки, так как же Rubygems знает, какую из них использовать? Это может занять самую последнюю версию, но затем, когда вы $ gem install более новую версию для другого проекта, ваш проект может сломаться. Rubygems пытается принять разумное решение относительно необходимых вам зависимостей на основе других зависимостей. Когда драгоценный камень публикуется на rubygems.org (сервер), он содержит .gemspec который объявляет информацию о текущем драгоценном камне, а также о зависимостях, которые имеет этот драгоценный камень. Например, в wicked.gemspec

 gem.add_dependency "railties", [">= 3.0.7"] gem.add_development_dependency "rails", [">= 3.0.7"] gem.add_development_dependency "capybara", [">= 0"] 

Если вам нужна только эта версия wicked , она не может потребоваться, если вы не используете версию railties более 3.0.7 . Если он не может найти его, это вызовет исключение. Если он находит верную версию, он добавляет ее в $LOADED_FEATURES .

Используя другую библиотеку с threaded , мы можем увидеть $LOADED_FEATURES , вот она перед загрузкой:

 puts $LOADED_FEATURES.inspect => ["enumerator.so", "rational.so", "complex.so", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/enc/encdb.bundle", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/enc/trans/transdb.bundle", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/unicode_normalize.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/rbconfig.rb", "thread.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/thread.bundle", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/compatibility.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/defaults.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/deprecate.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/errors.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/version.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/requirement.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/platform.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/basic_specification.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/stub_specification.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/util/stringio.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/specification.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/exceptions.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/core_ext/kernel_gem.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/monitor.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/e2mmap.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/init.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/workspace.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/inspector.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/context.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/extend-command.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/output-method.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/notifier.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/slex.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/ruby-token.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/ruby-lex.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/src_encoding.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/magic-file.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/readline.bundle", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/input-method.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/locale.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/path_support.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/dependency.rb"] 

И после:

 ["enumerator.so", "rational.so", "complex.so", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/enc/encdb.bundle", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/enc/trans/transdb.bundle", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/unicode_normalize.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/rbconfig.rb", "thread.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/thread.bundle", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/compatibility.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/defaults.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/deprecate.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/errors.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/version.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/requirement.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/platform.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/basic_specification.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/stub_specification.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/util/stringio.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/specification.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/exceptions.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/core_ext/kernel_gem.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/monitor.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/e2mmap.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/init.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/workspace.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/inspector.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/context.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/extend-command.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/output-method.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/notifier.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/slex.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/ruby-token.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/ruby-lex.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/src_encoding.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/magic-file.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/readline.bundle", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/input-method.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb/locale.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/irb.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/path_support.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/rubygems/dependency.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/timeout.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/logger.rb", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14/stringio.bundle", "/Users/richardschneeman/.gem/ruby/2.2.1/gems/threaded-0.0.4/lib/threaded/version.rb", "/Users/richardschneeman/.gem/ruby/2.2.1/gems/threaded-0.0.4/lib/threaded/errors.rb", "/Users/richardschneeman/.gem/ruby/2.2.1/gems/threaded-0.0.4/lib/threaded/ext/stdout.rb", "/Users/richardschneeman/.gem/ruby/2.2.1/gems/threaded-0.0.4/lib/threaded/worker.rb", "/Users/richardschneeman/.gem/ruby/2.2.1/gems/threaded-0.0.4/lib/threaded/master.rb", "/Users/richardschneeman/.gem/ruby/2.2.1/gems/threaded-0.0.4/lib/threaded/promise.rb", "/Users/richardschneeman/.gem/ruby/2.2.1/gems/threaded-0.0.4/lib/threaded.rb"] 

Если вы беспокоитесь о том, что Rubygems может выбрать неправильную версию гема, вам нужно будет использовать bundler , который уже есть у большинства разработчиков Ruby. Bundler выполняет разрешение gem при первом запуске bundle install . Bundler использует явные спецификации из Gemfile вместе с информацией, содержащейся в спецификации Gemfile для отдельного камня. После разрешения Gemfile.lock генерирует Gemfile.lock . Разработчики проверяют этот файл в своей системе контроля версий, чтобы убедиться, что каждая версия, которую они устанавливают, совпадает с версией их коллег. В противном случае вы получите тонкие баги «все работает на моей машине», которые существуют между незначительными различиями версий программного обеспечения. Bundler работает с Rubygems, явно добавляя все гемы из Gemfile.lock в $LOAD_PATH

 $ echo Gemfile source 'https://rubygems.org' gem 'threaded' $ bundle exec irb > puts $LOAD_PATH # => ["/Users/richardschneeman/.gem/ruby/2.2.1/gems/threaded-0.0.4/lib", "/Users/richardschneeman/.gem/ruby/2.2.1/gems/bundler-1.8.3/lib", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/site_ruby/2.2.0", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/site_ruby/2.2.0/x86_64-darwin14", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/site_ruby", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/vendor_ruby/2.2.0", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/vendor_ruby/2.2.0/x86_64-darwin14", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/vendor_ruby", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0", "/Users/richardschneeman/.rubies/ruby-2.2.1/lib/ruby/2.2.0/x86_64-darwin14"] 

Здесь вы можете увидеть, что многопоточная версия 0.0.4 уже находится на пути загрузки. Теперь, когда вы попытаетесь require threaded , эта явная версия будет найдена, и Rubygems не будет пытаться выполнить разрешение.

Было бы похоже, что использование Gemfile.lock с Gemfile.lock устраняет необходимость в Rubygems для разрешения зависимостей. Это в основном верно, но все еще достаточно много раз, когда вам нужно require корректной работы без Gemfile.lock . Хорошим примером является запуск rails new foo . Эта команда фактически сгенерирует для вас Gemfile и Gemfile.lock . Это именно то, что делал @mfazekas, когда была обнаружена проблема .

Rubygems Internals

Что на самом деле происходит, когда вам требуется гем в вашем Ruby-коде? Когда вы вызываете require "wicked" , код сначала попытается найти wicked в обычной стандартной библиотеке. Если это невозможно, то Rubygems увидит, был ли он загружен ранее. Если wicked ранее не требовался, Rubygems должен найти его и вызвать:

 found_specs = Gem::Specification.find_in_unresolved path 

В Rubygems мы «активируем» гем, когда точно знаем, какая версия нужна и присутствует в системе. После активации мы не можем активировать другую версию этого драгоценного камня. Rubygems работает для разрешения коллизий зависимостей, когда это возможно, и выдает ошибку, когда не может.

Внутри Rubygems «неразрешенная» спецификация драгоценного камня — это драгоценный камень, на который ссылается другой драгоценный камень, но еще не загружен. Например, если у нас есть драгоценный камень a который требует драгоценный камень с именем b мы хотим, чтобы оба драгоценных камня были загружены, когда нам требуется a . Для того, чтобы это работало, Gem::Specification#activate вызывается на геме. Если бы это были только две необходимые зависимости, то мы могли бы «активировать» версию b которая нужна. Однако, если есть другие драгоценные камни, о которых система знает, что они не были активированы с этой ссылкой b , они еще не могут быть разрешены и загружены. Когда это происходит, требования к зависимости хранятся в памяти:

 > puts Gem::Specification.unresolved_deps.inspect [<Gem::Dependency type=:runtime name="b" requirements=">= 0">] 

Позже, когда gem явно require -d, мы проверяем, был ли он ранее сохранен в нашем списке Gem::Specification#find_in_unresolved .

Если мы пытаемся require 'c' где активирован a и ссылается на b (что является «неразрешенным»), то Rubygems пока не знает, где найти c . Для этого ему нужно пройти через все неразрешенные зависимости и найти ссылки на c . Для этого мы звоним:

 found_specs = Gem::Specification.find_in_unresolved_tree path 

который должен пройти все возможные драгоценные камни.

Gem Traverse

Метод обхода внутри Rubygems является дорогостоящим и не связан с Chevy.

chevy traverse

(стон)

При попытке найти спецификацию в неразрешенном дереве необходимо найти неразрешенные зависимости и проверить каждую из их зависимостей, чтобы узнать, содержат ли они искомый камень.

Вы можете увидеть эту логику в Gem::Specification.find_in_unresolved_tree :

 def self.find_in_unresolved_tree path specs = unresolved_deps.values.map { |dep| dep.to_specs }.flatten specs.reverse_each do |spec| trails = [] spec.traverse do |from_spec, dep, to_spec, trail| next unless to_spec.conflicts.empty? trails << trail if to_spec.contains_requirable_file? path end next if trails.empty? return trails.map(&:reverse).sort.first.reverse end [] end 

Этот код вызывает метод traverse для каждой «неразрешенной» спецификации:

 def traverse trail = [], &block trail = trail + [self] runtime_dependencies.each do |dep| dep.to_specs.each do |dep_spec| block[self, dep, dep_spec, trail + [dep_spec]] dep_spec.traverse(trail, &block) unless trail.map(&:name).include? dep_spec.name end end end 

Этот метод принимает все Gem::Specification -s, которые будут автоматически активированы целевым гемом во время выполнения; они известны как «зависимости времени выполнения». Для каждого из них он захватывает их зависимости и вызывает traverse для каждой из зависимостей. Каждый раз, когда это происходит, переменная trail добавляется с текущей Gem::Specification .

Это дорогой метод, так как он выделяет 2 массива каждый раз, когда он вызывается:

 trail = trail + [self] 

В дополнение к массиву [self] процесс добавления массива к уже существующему массиву фактически выделяет новый массив, даже если вы используете ту же переменную:

 irb(main):005:0> array = [1,2,3] => [1, 2, 3] irb(main):006:0> puts array.object_id 70265837266320 => nil irb(main):007:0> array = array + [4] => [1, 2, 3, 4] irb(main):008:0> puts array.object_id 70265837200880 

Как только мы сделаем присваивание array = array + [4] , object_id переменной array изменится. Новый массив был выделен. Интерпретатор Ruby может оптимизировать, чтобы не выделять этот массив, но это очень сложно, так как где-то еще может храниться ссылка на массив, включая код с eval кодом:

 array = [1, 2, 3] eval( <<-CODE def foo(ary); $array = ary; end foo(array) CODE ) > array = array + [4] 

Дублирование этого массива снова и снова при каждом traverse становится еще хуже, когда это будет сделано позже:

 block[self, dep, dep_spec, trail + [dep_spec]] 

Примечание. Этот код идентичен вызову block.call(self, dep, dep_spec, trail + [dep_spec])

Список на помощь?

Массив содержит довольно много функций, которые не нужны в этом случае. Все, что мы пытаемся сделать, это отслеживать спецификации, которые были пройдены. Аарон представил относительно популярный патч, чтобы помочь с этой проблемой:

Gem::List — это структура данных связанного списка . Эта структура похожа на массив в том, что она содержит последовательные данные. В связанном списке элементы могут быть добавлены и удалены очень эффективно по сравнению с массивом, поскольку всю структуру данных не нужно реорганизовывать. Связанный список состоит из множества узлов, каждый из которых имеет значение и ссылку на «следующий» или «хвостовой» узел. Вы можете перебирать весь связанный список, например, из источника Gem :: Node :

 def each n = self while n yield n.value n = n.tail end end 

В запросе pull этот Gem::Node используется для отслеживания процесса обхода. Посмотрите на источник:

 ## # This method is for traversing spec dependencies. Don't use this, it is # super private. I am super serious! def self._traverse spec, trail, &block # :nodoc: spec.dependencies.each do |dep| next unless dep.runtime? dep.to_specs.each do |dep_spec| stack = Gem::List.new(dep_spec, trail) block[dep_spec, stack] spec_name = dep_spec.name _traverse(dep_spec, stack, &block) unless stack.any? { |s| s.name == spec_name } end end end private_class_method :_traverse 

Логика, по сути, такая же, как и исходный ход. Рекурсивно просмотрите все спецификации драгоценных камней и просмотрите каждую их спецификацию. В то время как учет «миллионов выделений» был вызван ошибкой в ​​PR, улучшение по-прежнему заключается в том, что сокращение выделений при каждом вызове является улучшением.

Последствия

Тест, который был написан, выдвигает на первый план худший вариант развития событий. У нас есть много драгоценных камней, которые не были «активированы», но загружены, и они также ссылаются на многие другие драгоценные камни. Мы навязываем полный обход путем поиска файла, который не существует.

Что это значит для вашего приложения? Я изменил Rubygems, чтобы выводить количество вызовов Gem::Specification.find_in_unresolved_tree path при загрузке моего приложения codetriage.com . Этот метод вызывается дважды, что приводит к 30 общим вызовам метода traverse .

Вы можете уменьшить это до 0 вызовов любого метода, загрузив приложение с помощью bundle exec . Когда используется bundle exec , область действия драгоценных камней, по которым можно искать, уменьшается, поскольку в пути Gemfile.lock будут присутствовать только камни, найденные в Gemfile.lock .

Что все это значит?

Это один из моих любимых недавних запросов на выборку по ряду причин. Во-первых, в нем была ошибка, которую никто не поймал в тестах. Что это значит? Мы не всегда можем полагаться на существующие тестовые наборы для обработки каждого возможного сценария. Есть причина, по которой такие запросы на получение доступа делаются публично. Человеческий элемент для анализа и оценки достоверности нельзя недооценивать.

Моя вторая любимая вещь в этом запросе на выбор — то, что тесты, используемые для определения экономии, были включены в описание. Это означает не только то, что мы все можем учиться у пиара, но и все мы можем проверить это самостоятельно. Эта открытость позволила любому запустить код, применить исправление и отдельно проверить экономию. Именно это и привлекло меня к исследованию поведения Rubygems. Этот пост основан на необработанных заметках, которые я сделал при оценке запроса на извлечение . Наконец, мне очень нравится, что эта работа с производительностью произошла из поиска и сообщения об ошибках в производительности. Почему мы, как Rubyists, не тратим половину времени на сравнительный анализ и поиск ошибок в производительности, жалуясь на медлительность?

Я написал свой первый черновик этой статьи прямо перед анонсом RubyTogether, который стремится сделать много вещей, в том числе помочь сделать rubygems.org устойчивым .

Для людей вне основных разработчиков проекта важно перечитать и понять популярные запросы на извлечение. Для вас это отличный способ узнать о новых инструментах и ​​методах, но также самое время узнать о том, как работают различные внутренние части проектов. Я призываю вас найти пиар, который вы не совсем понимаете: сделайте заметки, покопайтесь и узнайте что-нибудь. Лучшая часть вклада с открытым исходным кодом заключается не только в том, что мы извлекаем выгоду из работы других, мы видим, как эта работа стала, и у нас есть шанс стать лучше.


Если вам нравится открытый исходный код и Ruby, следуйте @schneems . Если вы хотите разобраться в проблемах с открытым исходным кодом и получить запросы, подпишитесь на codetriage.com .