feedjira 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +9 -2
- data/CHANGELOG.md +4 -0
- data/LICENSE +1 -1
- data/README.md +210 -7
- data/Rakefile +5 -0
- data/feedjira.gemspec +2 -1
- data/lib/feedjira.rb +7 -1
- data/lib/feedjira/configuration.rb +76 -0
- data/lib/feedjira/core_ext/date.rb +1 -0
- data/lib/feedjira/core_ext/string.rb +1 -0
- data/lib/feedjira/core_ext/time.rb +5 -1
- data/lib/feedjira/date_time_utilities.rb +11 -3
- data/lib/feedjira/date_time_utilities/date_time_epoch_parser.rb +13 -0
- data/lib/feedjira/date_time_utilities/date_time_language_parser.rb +2 -0
- data/lib/feedjira/date_time_utilities/date_time_pattern_parser.rb +6 -1
- data/lib/feedjira/feed.rb +87 -69
- data/lib/feedjira/feed_entry_utilities.rb +5 -2
- data/lib/feedjira/feed_utilities.rb +11 -1
- data/lib/feedjira/parser.rb +1 -1
- data/lib/feedjira/parser/atom.rb +1 -0
- data/lib/feedjira/parser/atom_entry.rb +1 -0
- data/lib/feedjira/parser/atom_feed_burner.rb +19 -2
- data/lib/feedjira/parser/atom_feed_burner_entry.rb +1 -0
- data/lib/feedjira/parser/atom_youtube.rb +1 -0
- data/lib/feedjira/parser/atom_youtube_entry.rb +1 -0
- data/lib/feedjira/parser/google_docs_atom.rb +2 -1
- data/lib/feedjira/parser/google_docs_atom_entry.rb +2 -0
- data/lib/feedjira/parser/itunes_rss.rb +1 -0
- data/lib/feedjira/parser/itunes_rss_category.rb +1 -0
- data/lib/feedjira/parser/itunes_rss_owner.rb +1 -0
- data/lib/feedjira/parser/podlove_chapter.rb +2 -0
- data/lib/feedjira/parser/rss.rb +1 -0
- data/lib/feedjira/parser/rss_feed_burner.rb +1 -0
- data/lib/feedjira/parser/rss_feed_burner_entry.rb +1 -0
- data/lib/feedjira/preprocessor.rb +2 -0
- data/lib/feedjira/version.rb +1 -1
- data/spec/feedjira/configuration_spec.rb +25 -0
- data/spec/feedjira/date_time_utilities_spec.rb +6 -0
- data/spec/feedjira/feed_spec.rb +20 -2
- data/spec/feedjira/feed_utilities_spec.rb +18 -0
- data/spec/feedjira/parser/atom_feed_burner_spec.rb +32 -1
- data/spec/sample_feeds.rb +1 -0
- data/spec/sample_feeds/GiantRobotsSmashingIntoOtherGiantRobots.xml +682 -0
- metadata +49 -29
data/lib/feedjira/version.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Feedjira::Configuration do
|
4
|
+
describe '.configure' do
|
5
|
+
it 'sets follow_redirect_limit config' do
|
6
|
+
Feedjira.configure { |config| config.follow_redirect_limit = 10 }
|
7
|
+
expect(Feedjira.follow_redirect_limit).to eq(10)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'sets request_timeout config' do
|
11
|
+
Feedjira.configure { |config| config.request_timeout = 45 }
|
12
|
+
expect(Feedjira.request_timeout).to eq(45)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'sets strip_whitespace config' do
|
16
|
+
Feedjira.configure { |config| config.strip_whitespace = true }
|
17
|
+
expect(Feedjira.strip_whitespace).to be true
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'sets user_agent config' do
|
21
|
+
Feedjira.configure { |config| config.user_agent = 'Test User Agent' }
|
22
|
+
expect(Feedjira.user_agent).to eq('Test User Agent')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -37,5 +37,11 @@ describe Feedjira::FeedUtilities do
|
|
37
37
|
expect(time.class).to eq Time
|
38
38
|
expect(time).to eq Time.parse_safely('Wed Aug 31 14:37:00 UTC 2016')
|
39
39
|
end
|
40
|
+
|
41
|
+
it 'should parse epoch into Time' do
|
42
|
+
time = @klass.new.parse_datetime('1472654220')
|
43
|
+
expect(time.class).to eq Time
|
44
|
+
expect(time).to eq Time.parse_safely('Wed Aug 31 14:37:00 UTC 2016')
|
45
|
+
end
|
40
46
|
end
|
41
47
|
end
|
data/spec/feedjira/feed_spec.rb
CHANGED
@@ -224,8 +224,26 @@ describe Feedjira::Feed do
|
|
224
224
|
parser = Feedjira::Feed.determine_feed_parser_for_xml xml
|
225
225
|
expect(parser).to eq new_parser
|
226
226
|
|
227
|
-
|
228
|
-
|
227
|
+
Feedjira::Feed.reset_parsers!
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe 'when parsers are configured' do
|
232
|
+
it 'does not use default parsers' do
|
233
|
+
xml = 'Atom asdf'
|
234
|
+
new_parser = Class.new do
|
235
|
+
def self.able_to_parse?(_)
|
236
|
+
true
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
Feedjira.configure { |config| config.parsers = [new_parser] }
|
241
|
+
|
242
|
+
parser = Feedjira::Feed.determine_feed_parser_for_xml(xml)
|
243
|
+
expect(parser).to eq(new_parser)
|
244
|
+
|
245
|
+
Feedjira.reset_configuration!
|
246
|
+
Feedjira::Feed.reset_parsers!
|
229
247
|
end
|
230
248
|
end
|
231
249
|
end
|
@@ -26,6 +26,24 @@ describe Feedjira::FeedUtilities do
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
describe 'strip whitespace' do
|
30
|
+
context 'strip_whitespace config is true' do
|
31
|
+
it 'strips all XML whitespace' do
|
32
|
+
Feedjira.configure { |config| config.strip_whitespace = true }
|
33
|
+
|
34
|
+
expect(@klass.strip_whitespace("\nfoobar\n")).to eq('foobar')
|
35
|
+
|
36
|
+
Feedjira.configure { |config| config.strip_whitespace = false }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'strip_whitespace config is false' do
|
41
|
+
it 'lstrips XML whitespace' do
|
42
|
+
expect(@klass.strip_whitespace("\nfoobar\n")).to eq("foobar\n")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
29
47
|
describe 'instance methods' do
|
30
48
|
it 'should provide an updated? accessor' do
|
31
49
|
feed = @klass.new
|
@@ -19,7 +19,7 @@ module Feedjira::Parser
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
describe 'parsing' do
|
22
|
+
describe 'parsing old style feeds' do
|
23
23
|
before(:each) do
|
24
24
|
@feed = AtomFeedBurner.parse(sample_feedburner_atom_feed)
|
25
25
|
end
|
@@ -56,6 +56,37 @@ module Feedjira::Parser
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
+
describe 'parsing alternate style feeds' do
|
60
|
+
before(:each) do
|
61
|
+
@feed = AtomFeedBurner.parse(sample_feedburner_atom_feed_alternate)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should parse the title' do
|
65
|
+
expect(@feed.title).to eq 'Giant Robots Smashing Into Other Giant Robots'
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should parse the description' do
|
69
|
+
description = 'Written by thoughtbot'
|
70
|
+
expect(@feed.description).to eq description
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should parse the url' do
|
74
|
+
expect(@feed.url).to eq 'https://robots.thoughtbot.com'
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should parse the feed_url' do
|
78
|
+
expect(@feed.feed_url).to eq 'http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots'
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should parse hub urls' do
|
82
|
+
expect(@feed.hubs.count).to eq 1
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should parse entries' do
|
86
|
+
expect(@feed.entries.size).to eq 3
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
59
90
|
describe 'preprocessing' do
|
60
91
|
it 'retains markup in xhtml content' do
|
61
92
|
AtomFeedBurner.preprocess_xml = true
|
data/spec/sample_feeds.rb
CHANGED
@@ -19,6 +19,7 @@ module SampleFeeds
|
|
19
19
|
sample_rss_feed: 'TenderLovemaking.xml',
|
20
20
|
sample_rss_entry_content: 'TenderLovemakingFirstEntry.xml',
|
21
21
|
sample_feedburner_atom_feed: 'PaulDixExplainsNothing.xml',
|
22
|
+
sample_feedburner_atom_feed_alternate: 'GiantRobotsSmashingIntoOtherGiantRobots.xml',
|
22
23
|
sample_feedburner_atom_entry_content: 'PaulDixExplainsNothingFirstEntryContent.xml',
|
23
24
|
sample_wfw_feed: 'PaulDixExplainsNothingWFW.xml',
|
24
25
|
sample_google_docs_list_feed: 'GoogleDocsList.xml',
|
@@ -0,0 +1,682 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/atom10full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0">
|
3
|
+
<title>Giant Robots Smashing Into Other Giant Robots</title>
|
4
|
+
<subtitle>Written by thoughtbot</subtitle>
|
5
|
+
<id>https://robots.thoughtbot.com/</id>
|
6
|
+
<link href="https://robots.thoughtbot.com" />
|
7
|
+
|
8
|
+
<updated>2016-12-21T00:00:00+00:00</updated>
|
9
|
+
<author>
|
10
|
+
<name>thoughtbot</name>
|
11
|
+
</author>
|
12
|
+
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/atom+xml" href="http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots" /><feedburner:info uri="giantrobotssmashingintoothergiantrobots" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><feedburner:emailServiceId>GiantRobotsSmashingIntoOtherGiantRobots</feedburner:emailServiceId><feedburner:feedburnerHostname>https://feedburner.google.com</feedburner:feedburnerHostname><feedburner:browserFriendly>This is an XML content feed. It is intended to be viewed in a newsreader or syndicated to another site, subject to copyright and fair use.</feedburner:browserFriendly><entry>
|
13
|
+
<title>"Tell, Don't Ask" in Elixir: A Story of Pattern-Matching
|
14
|
+
</title>
|
15
|
+
<link rel="alternate" href="http://feedproxy.google.com/~r/GiantRobotsSmashingIntoOtherGiantRobots/~3/DeWX2QCae6A/tell-don-t-ask-in-elixir" />
|
16
|
+
<author>
|
17
|
+
<name>Josh Clayton</name>
|
18
|
+
</author>
|
19
|
+
<id>https://robots.thoughtbot.com/tell-don-t-ask-in-elixir</id>
|
20
|
+
<published>2016-12-21T00:00:00+00:00</published>
|
21
|
+
<updated>2016-12-20T22:08:11Z</updated>
|
22
|
+
<content type="html"><p><a href="https://robots.thoughtbot.com/tell-dont-ask">&ldquo;Tell, Don&rsquo;t Ask&rdquo;</a> is a <a href="http://martinfowler.com/bliki/TellDontAsk.html">well-covered</a> <a href="http://c2.com/cgi/wiki?TellDontAsk">topic</a> within
|
23
|
+
object-oriented programming communities. Its goal? Encourage encapsulation by
|
24
|
+
having the caller <strong>tell</strong> an object to do something instead of checking on
|
25
|
+
state and acting upon it. Almost at odds with the <a href="https://github.com/troessner/reek/blob/master/docs/Control-Couple.md">control couple</a> code smell,
|
26
|
+
our goal is to have the caller issue explicit commands without concerning
|
27
|
+
itself with object state.</p>
|
28
|
+
<h2 id="quottell-don39t-askquot-in-elixir">
|
29
|
+
<a href="#quottell-don39t-askquot-in-elixir">
|
30
|
+
&ldquo;Tell, Don&rsquo;t Ask&rdquo; in Elixir
|
31
|
+
</a>
|
32
|
+
</h2>
|
33
|
+
|
34
|
+
<p><a href="http://tech.noredink.com/post/142689001488/the-most-object-oriented-language">Is Elixir object-oriented?</a> From a paradigm perspective, Elixir is a
|
35
|
+
functional language when looking at aspects like immutability,
|
36
|
+
pattern-matching, and functions with inputs and outputs, focused on the sending
|
37
|
+
of messages to &ldquo;objects&rdquo; directly. How does &ldquo;Tell, Don&rsquo;t Ask&rdquo; translate?</p>
|
38
|
+
|
39
|
+
<p>Thinking about the goal, let&rsquo;s do some mental mapping. In OOP, objects are a
|
40
|
+
blueprint with information containing behavior (methods) and data (state).
|
41
|
+
In FP, we have functions organized within modules, with state being captured in
|
42
|
+
various values (e.g. Elixir&rsquo;s Maps or Structs). We want to avoid having the
|
43
|
+
caller (a function) dictate paths based on information present in our data.</p>
|
44
|
+
|
45
|
+
<p>Let&rsquo;s write out some non-idiomatic Elixir and see what we can improve.</p>
|
46
|
+
|
47
|
+
<pre><code class="elixir">defmodule Game.Lobby do
|
48
|
+
def add_player(%{game: game} = lobby, player) do
|
49
|
+
new_player = cond do
|
50
|
+
is_binary(player) -&gt;
|
51
|
+
%Game.Player{name: player, id: Game.Player.generate_id}
|
52
|
+
is_map(player) -&gt;
|
53
|
+
%Game.Player{} |&gt; Map.merge(player)
|
54
|
+
true -&gt;
|
55
|
+
%Game.Player{}
|
56
|
+
end
|
57
|
+
|
58
|
+
%{lobby |
|
59
|
+
game: %{game | players: game.players ++ [new_player]}}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
defmodule Game.Player do
|
64
|
+
defstruct id: 0, name: &quot;New player&quot;, active: true
|
65
|
+
|
66
|
+
def generate_id do
|
67
|
+
UUID.uuid4()
|
68
|
+
end
|
69
|
+
end
|
70
|
+
</code></pre>
|
71
|
+
|
72
|
+
<p><code>Game.Lobby.add_player/2</code> doesn&rsquo;t feel right. There&rsquo;s a significant amount of
|
73
|
+
<a href="http://c2.com/cgi/wiki?FeatureEnvySmell">feature envy</a> as it cares about the various shapes of <code>player</code> and how to
|
74
|
+
construct a <code>%Game.Player{}</code>. Also, why is <code>Game.Player.generate_id/0</code> public?
|
75
|
+
It seems all <code>Game.Lobby.add_player/2</code> should care about is managing its own
|
76
|
+
structure (the final two lines of the function).</p>
|
77
|
+
|
78
|
+
<p>Instead of having <code>Game.Lobby.add_player/2</code> care about constructing players,
|
79
|
+
generating ids, and so on, let&rsquo;s <strong>tell</strong> <code>Game.Player</code> to handle that instead:</p>
|
80
|
+
|
81
|
+
<pre><code class="elixir">defmodule Game.Lobby do
|
82
|
+
def add_player(%{game: game} = lobby, player) do
|
83
|
+
%{lobby |
|
84
|
+
game: %{game | players: game.players ++ [Game.Player.new(player)]}}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
defmodule Game.Player do
|
89
|
+
defstruct id: 0, name: &quot;New player&quot;, active: true
|
90
|
+
|
91
|
+
def new(name) when is_binary(name), do: new(%{name: name, id: generate_id})
|
92
|
+
def new(a) when is_map(a), do: %__MODULE__{} |&gt; Map.merge(a)
|
93
|
+
def new(_), do: %__MODULE__{}
|
94
|
+
|
95
|
+
defp generate_id, do: UUID.uuid4()
|
96
|
+
end
|
97
|
+
</code></pre>
|
98
|
+
|
99
|
+
<p>Here, we move player generation to the <code>Game.Player</code> module, where it can
|
100
|
+
determine how best to generate the struct instead of <code>Game.Lobby.add_player/2</code>.</p>
|
101
|
+
<h2 id="write-declarative-not-imperative-code">
|
102
|
+
<a href="#write-declarative-not-imperative-code">
|
103
|
+
Write declarative (not imperative) code
|
104
|
+
</a>
|
105
|
+
</h2>
|
106
|
+
|
107
|
+
<p>By moving player creation logic from <code>Game.Lobby.add_player/2</code> to
|
108
|
+
<code>Game.Player.new/1</code>, we were able to call a single function to take the
|
109
|
+
appropriate action based on data. It is important to note that the data it&rsquo;s
|
110
|
+
acting upon specifically is behavior to construct a <code>%Game.Player{}</code>.</p>
|
111
|
+
|
112
|
+
<p>This becomes more important when using the <a href="http://elixir-lang.org/docs/stable/elixir/Kernel.html#%7C%3E/2">pipe operator</a>, which shines as a
|
113
|
+
way to transform data.</p>
|
114
|
+
|
115
|
+
<p>&ldquo;Tell, don&rsquo;t ask&rdquo; is a way to encourage developers to <a href="https://medium.freecodecamp.com/imperative-vs-declarative-programming-283e96bf8aea">write declarative code
|
116
|
+
instead of imperative code</a>. Imperative code asks questions before making
|
117
|
+
decisions; declarative code issues a command and expects it to be done
|
118
|
+
correctly.</p>
|
119
|
+
</content>
|
120
|
+
<summary>Write declarative Elixir code by applying &ldquo;Tell, Don&rsquo;t Ask&rdquo;.</summary>
|
121
|
+
<feedburner:origLink>https://robots.thoughtbot.com/tell-don-t-ask-in-elixir</feedburner:origLink></entry>
|
122
|
+
<entry>
|
123
|
+
<title>Avoiding Out of Memory Crashes on Mobile</title>
|
124
|
+
<link rel="alternate" href="http://feedproxy.google.com/~r/GiantRobotsSmashingIntoOtherGiantRobots/~3/Px1_JC1CMVg/avoiding-out-of-memory-crashes-on-mobile" />
|
125
|
+
<author>
|
126
|
+
<name>Steff Kelsey</name>
|
127
|
+
</author>
|
128
|
+
<id>https://robots.thoughtbot.com/avoiding-out-of-memory-crashes-on-mobile</id>
|
129
|
+
<published>2016-12-20T00:00:00+00:00</published>
|
130
|
+
<updated>2016-12-19T19:30:45Z</updated>
|
131
|
+
<content type="html"><p>One reason why it is difficult to develop software for mobile devices is that
|
132
|
+
the hardware is not the best compared to deploying to a console or a &ldquo;real&rdquo;
|
133
|
+
computer. Resources are limited. One particularly sparse resource is RAM. Out
|
134
|
+
of memory exceptions are common on both Android and iOS if you&rsquo;re dealing with
|
135
|
+
large files. Recently, when building a Google VR 360 video player, we went over
|
136
|
+
the 1GB of RAM available on older iOS devices pretty quickly.</p>
|
137
|
+
<h2 id="what-not-to-do">
|
138
|
+
<a href="#what-not-to-do">
|
139
|
+
What Not to Do
|
140
|
+
</a>
|
141
|
+
</h2>
|
142
|
+
|
143
|
+
<p>One of my big complaints about the Unity manual and many tutorials is they
|
144
|
+
usually just show you how to do something really quickly and don&rsquo;t always tell
|
145
|
+
you the exact use case or how it can just flat out fail. For example, using the
|
146
|
+
relatively new <code>UnityWebRequest</code>, you can download a file over HTTP like this:</p>
|
147
|
+
|
148
|
+
<pre><code class="csharp">private IEnumerator loadAsset(string path)
|
149
|
+
{
|
150
|
+
using (UnityWebRequest webRequest = new UnityWebRequest(path))
|
151
|
+
{
|
152
|
+
webRequest.downloadHandler = new DownloadHandlerBuffer();
|
153
|
+
webRequest.Send();
|
154
|
+
while (!webRequest.isDone)
|
155
|
+
{
|
156
|
+
yield return null;
|
157
|
+
}
|
158
|
+
if (string.IsNullOrEmpty(webRequest.error))
|
159
|
+
{
|
160
|
+
FileComplete(this, new FileLoaderCompleteEventArgs(
|
161
|
+
webRequest.downloadHandler.data));
|
162
|
+
}
|
163
|
+
else
|
164
|
+
{
|
165
|
+
Debug.Log(&quot;error! message: &quot; + webRequest.error);
|
166
|
+
}
|
167
|
+
}
|
168
|
+
}
|
169
|
+
</code></pre>
|
170
|
+
|
171
|
+
<p>These are all off the shelf parts from Unity, with the exception of the
|
172
|
+
<code>FileLoaderCompleteEventArg</code> but just assume that we use that to pass off the
|
173
|
+
downloaded bytes as an array eg: <code>byte[]</code>. Notice this returns an <code>IEnumerator</code>
|
174
|
+
and utilizes <code>yield</code> statements so it should be run in a <code>Coroutine</code>. What
|
175
|
+
happens here is that the <code>UnityWebRequest</code> will open up a connection to the
|
176
|
+
given path, download everything into a byte array contained within the
|
177
|
+
<code>DownloadHandlerBuffer</code>. The <code>FileComplete</code> event will fire if there are no
|
178
|
+
errors, sending the entire byte array to the subscribing class. Easy, right? For
|
179
|
+
small files, sure. But we were making a 360 Video player. Our max resolution was
|
180
|
+
1440p. The first sample files we got for testing were bigger than 400MB. The
|
181
|
+
iPhone 7, with 2GB of RAM, took it like a champ. The iPhone 6, with 1GB of RAM,
|
182
|
+
crashed like a piano dropped from a helicopter.</p>
|
183
|
+
<h2 id="why-did-my-app-just-crash">
|
184
|
+
<a href="#why-did-my-app-just-crash">
|
185
|
+
Why Did my App Just Crash?
|
186
|
+
</a>
|
187
|
+
</h2>
|
188
|
+
|
189
|
+
<p>Let&rsquo;s look at the guts of these components. The main focus is on the
|
190
|
+
<code>DownloadHandlerBuffer</code> object. When it is first created, it will start by
|
191
|
+
preallocating memory for a small byte array where it will store all the
|
192
|
+
downloaded bytes. As the bytes come in, it will periodically expand the size of
|
193
|
+
the array. In our test case, it was expanding the array until it could hold
|
194
|
+
400MB. And because each additional allocation is a guess, it will most likely
|
195
|
+
overshot that amount. Note, I am speculating here because I have not looked at
|
196
|
+
the source code for the <code>DownloadBufferHandler</code>. There is a chance it allocates
|
197
|
+
space based on the Content-Length header returned with the HTTP Response. But,
|
198
|
+
the result is the same; it will use up at least 400MB of RAM. That&rsquo;s 40% of the
|
199
|
+
1GB that the iPhone 6 has! We&rsquo;re already in dangerous territory. I know what
|
200
|
+
you&rsquo;re saying, &ldquo;Steff, why did it crash if we only used 40% of the RAM?&rdquo; There
|
201
|
+
are two ways to find the answer. One (and give Unity credit here) is in the
|
202
|
+
documentation for <a href="https://docs.unity3d.com/ScriptReference/Networking.DownloadHandlerBuffer.html"><code>DownloadHandlerBuffer</code></a>.</p>
|
203
|
+
|
204
|
+
<blockquote>
|
205
|
+
<p>Note: When accessing DownloadHandler.data or DownloadHandler.text on this
|
206
|
+
subclass, a new byte array or string will be allocated each time the property
|
207
|
+
is accessed.</p>
|
208
|
+
</blockquote>
|
209
|
+
|
210
|
+
<p>So, by accessing the data property, Unity allocates an <em>additional 400MB of
|
211
|
+
memory</em> to pass off the byte array into the EventArg. Now we have used 800MB of
|
212
|
+
RAM just on handling this one file. The OS has other services running plus you
|
213
|
+
very likely have RAM allocated for bitmaps and UI and logic. You&rsquo;re doomed!</p>
|
214
|
+
<h2 id="profiling-memory-allocations">
|
215
|
+
<a href="#profiling-memory-allocations">
|
216
|
+
Profiling Memory Allocations
|
217
|
+
</a>
|
218
|
+
</h2>
|
219
|
+
|
220
|
+
<p>If you didn&rsquo;t read the docs, and they&rsquo;re long: I get it, you could have found
|
221
|
+
this memory leak by running the application in Unity while using the Profiler
|
222
|
+
<em>AND</em> by running the application on an iOS device while using a valuable free
|
223
|
+
tool from Apple: Instruments. The Allocations instrument captures information
|
224
|
+
about memory allocation for an application. I recommend using the Unity Profiler
|
225
|
+
heavily for testing in the Editor and then continuing performance testing on
|
226
|
+
device for each platform. They all act differently. Using the Profiler in the
|
227
|
+
Editor is only your first line of defense. In this case I only properly
|
228
|
+
understood what was happening when I watched it unfold in a recording using the
|
229
|
+
Allocations instrument.</p>
|
230
|
+
<h2 id="streams-to-the-rescue">
|
231
|
+
<a href="#streams-to-the-rescue">
|
232
|
+
Streams to the Rescue
|
233
|
+
</a>
|
234
|
+
</h2>
|
235
|
+
|
236
|
+
<p>There is a way to download large files and save them without using unnecessary
|
237
|
+
RAM. <em>Streams!</em> Since we plan on immediately saving these large video files
|
238
|
+
in local storage on device to be ready for offline viewing, we need to send the
|
239
|
+
downloaded bytes right into a File as they are received. When doing that, we can
|
240
|
+
reuse the same byte array and never have to allocate more space. Unity outlines
|
241
|
+
how to do that <a href="https://docs.unity3d.com/Manual/UnityWebRequest-CreatingDownloadHandlers.html">here</a>, but below is an expanded example that includes a <code>FileStream</code>:</p>
|
242
|
+
|
243
|
+
<pre><code class="csharp">public class ToFileDownloadHandler : DownloadHandlerScript
|
244
|
+
{
|
245
|
+
private int expected = -1;
|
246
|
+
private int received = 0;
|
247
|
+
private string filepath;
|
248
|
+
private FileStream fileStream;
|
249
|
+
private bool canceled = false;
|
250
|
+
|
251
|
+
public ToFileDownloadHandler(byte[] buffer, string filepath)
|
252
|
+
: base(buffer)
|
253
|
+
{
|
254
|
+
this.filepath = filepath;
|
255
|
+
fileStream = new FileStream(filepath, FileMode.Create, FileAccess.Write);
|
256
|
+
}
|
257
|
+
|
258
|
+
protected override byte[] GetData() { return null; }
|
259
|
+
|
260
|
+
protected override bool ReceiveData(byte[] data, int dataLength)
|
261
|
+
{
|
262
|
+
if (data == null || data.Length &lt; 1)
|
263
|
+
{
|
264
|
+
return false;
|
265
|
+
}
|
266
|
+
received += dataLength;
|
267
|
+
if (!canceled) fileStream.Write(data, 0, dataLength);
|
268
|
+
return true;
|
269
|
+
}
|
270
|
+
|
271
|
+
protected override float GetProgress()
|
272
|
+
{
|
273
|
+
if (expected &lt; 0) return 0;
|
274
|
+
return (float)received / expected;
|
275
|
+
}
|
276
|
+
|
277
|
+
protected override void CompleteContent()
|
278
|
+
{
|
279
|
+
fileStream.Close();
|
280
|
+
}
|
281
|
+
|
282
|
+
protected override void ReceiveContentLength(int contentLength)
|
283
|
+
{
|
284
|
+
expected = contentLength;
|
285
|
+
}
|
286
|
+
|
287
|
+
public void Cancel()
|
288
|
+
{
|
289
|
+
canceled = true;
|
290
|
+
fileStream.Close();
|
291
|
+
File.Delete(filepath);
|
292
|
+
}
|
293
|
+
}
|
294
|
+
</code></pre>
|
295
|
+
|
296
|
+
<p>And to use the above in our coroutine:</p>
|
297
|
+
|
298
|
+
<pre><code class="csharp">private IEnumerator loadAsset(string path, string savePath)
|
299
|
+
{
|
300
|
+
using (UnityWebRequest webRequest = new UnityWebRequest(path))
|
301
|
+
{
|
302
|
+
webRequest.downloadHandler = new ToFileDownloadHandler(new byte[64 * 1024],
|
303
|
+
savePath);
|
304
|
+
webRequest.Send();
|
305
|
+
...
|
306
|
+
...
|
307
|
+
}
|
308
|
+
}
|
309
|
+
</code></pre>
|
310
|
+
|
311
|
+
<p>Looking first at our new <code>ToFileDownloadHandler</code>, we extended Unity&rsquo;s
|
312
|
+
<code>DownloadHandlerScript</code> and have overridden the required methods. The magic
|
313
|
+
happens in two places. First, we pass in a byte array to the base class via the
|
314
|
+
constructor. This let&rsquo;s Unity know that we want to re-use that byte array on
|
315
|
+
each <code>ReceiveData</code> callback where we only allocate a small amount of RAM once.
|
316
|
+
Second, we use a <code>FileStream</code> object to write the bytes directly to our desired
|
317
|
+
file. The rest of the code is there to handle canceling the request. Whenever
|
318
|
+
you deal with <code>FileStream</code> objects, you <em>must</em> remember to close them out when
|
319
|
+
you&rsquo;re done.</p>
|
320
|
+
|
321
|
+
<p>Looking at the <code>loadAsset</code> method, we added a parameter for the path to where
|
322
|
+
the file will be saved locally and we defined the size of the buffer at 64MB.
|
323
|
+
This size is dependent on your network speeds. We were focussed on WiFi
|
324
|
+
connections, so a larger buffer made sense. Too small and you will make the
|
325
|
+
download take longer than necessary to complete.</p>
|
326
|
+
<h2 id="where-to-go-from-here">
|
327
|
+
<a href="#where-to-go-from-here">
|
328
|
+
Where to Go from Here
|
329
|
+
</a>
|
330
|
+
</h2>
|
331
|
+
|
332
|
+
<p>Now you have an understanding of one way that your application can eat up RAM.
|
333
|
+
If you only take away one thing from reading this post it&rsquo;s this: for managing
|
334
|
+
memory allocations, streams are your friends. And you should be constantly
|
335
|
+
performance testing as you develop your application, unless you&rsquo;re trying to
|
336
|
+
maximize one-star reviews in the App Store.</p>
|
337
|
+
<h2 id="gotchyas">
|
338
|
+
<a href="#gotchyas">
|
339
|
+
Gotchyas
|
340
|
+
</a>
|
341
|
+
</h2>
|
342
|
+
|
343
|
+
<p>One final note on the code above: we did not end up going to production using
|
344
|
+
<code>UnityWebRequest</code> on iOS. When we tried using a similar streaming solution
|
345
|
+
as above, we found that the request was not clearing from memory if it was
|
346
|
+
canceled due to the user sending the application to the background. Using the
|
347
|
+
Time Profiler Instrument showed that <code>NSURLSession</code> objects were not being
|
348
|
+
cleaned up when the application paused and resumed, so eventually the CPU would
|
349
|
+
max out and crash. We had to seek an alternative solution for iOS using a native
|
350
|
+
plugin. However, in the final code we still used HTTP streaming directly into a
|
351
|
+
file via <code>FileStream</code>. Just not wrapped up in <code>UnityWebRequest</code> objects.</p>
|
352
|
+
</content>
|
353
|
+
<summary>How to use Streams to more efficiently manage memory when downloading large files using Unity Engine.</summary>
|
354
|
+
<feedburner:origLink>https://robots.thoughtbot.com/avoiding-out-of-memory-crashes-on-mobile</feedburner:origLink></entry>
|
355
|
+
<entry>
|
356
|
+
<title>Solve Problems, not Strawmen</title>
|
357
|
+
<link rel="alternate" href="http://feedproxy.google.com/~r/GiantRobotsSmashingIntoOtherGiantRobots/~3/E6Xo6tl2AKk/you-aint-gonna-need-process" />
|
358
|
+
<author>
|
359
|
+
<name>Mike Burns</name>
|
360
|
+
</author>
|
361
|
+
<id>https://robots.thoughtbot.com/you-aint-gonna-need-process</id>
|
362
|
+
<published>2016-12-19T00:00:00+00:00</published>
|
363
|
+
<updated>2016-12-19T20:00:59Z</updated>
|
364
|
+
<content type="html"><p>It&rsquo;s important to remember that solutions make little sense without problems,
|
365
|
+
and to <strong>avoid applying solutions in a vacuum</strong>.</p>
|
366
|
+
|
367
|
+
<p>Applying solutions within a vacuum &ndash; for example, reading about VPNs and then
|
368
|
+
deciding to implement one for your team, without having a strong driving
|
369
|
+
impetus &ndash; has a few downsides. Skipping the problem statement makes it
|
370
|
+
difficult to measure whether the solution works or how to iterate on it. It
|
371
|
+
also runs the risk of decreasing team happiness when solutions are not tethered
|
372
|
+
to reality.</p>
|
373
|
+
|
374
|
+
<p>We avoid applying them unless needed in an effort to reduce bureaucracy and
|
375
|
+
process, so that developers can concentrate on developing, designers can
|
376
|
+
concentrate on designing, product owners can concentrate on prioritization, and
|
377
|
+
so on.</p>
|
378
|
+
|
379
|
+
<p>To round this out we&rsquo;ll present <a href="https://thoughtbot.com/playbook">some solutions that we apply to
|
380
|
+
problems</a>, along with some of the problems that they attack. Our
|
381
|
+
choice of the word &ldquo;attack&rdquo; here is meaningful: these solutions, despite their
|
382
|
+
name, do not <em>solve</em> the problems; rather, they are part of a long-running
|
383
|
+
process of dealing with the problems. They might solve them entirely; they
|
384
|
+
might need refinement, or they might not work at all.</p>
|
385
|
+
<h2 id="acceptance-criteria">
|
386
|
+
<a href="#acceptance-criteria">
|
387
|
+
Acceptance Criteria
|
388
|
+
</a>
|
389
|
+
</h2>
|
390
|
+
|
391
|
+
<p>Acceptance criteria is one way to attack the following problems:</p>
|
392
|
+
|
393
|
+
<ul>
|
394
|
+
<li>Unrelated result: the ticket says one thing, the dev does another. If you
|
395
|
+
have another person accepting the stories, this will lead to lost time as the
|
396
|
+
dev and QA go back and forth on solutions. Without QA, this leads to an app
|
397
|
+
that you don&rsquo;t recognize.</li>
|
398
|
+
<li>The banana ticket: the developer knows how to implement the solution, but
|
399
|
+
doesn&rsquo;t know <a href="http://blog.teamtreehouse.com/when-is-a-user-story-done-acceptance-criteria-definition-done">when to stop</a>, leading to an infinite refactoring of the entire
|
400
|
+
codebase under the guise of finishing this one ticket.</li>
|
401
|
+
</ul>
|
402
|
+
|
403
|
+
<p>To implement <a href="http://nomad8.com/acceptance_criteria/">acceptance criteria</a>, when writing tickets and stories for the
|
404
|
+
team, provide a detailed description of what the solution might look like &ndash; a
|
405
|
+
description of when the story is finished and the ticket can be accepted by the
|
406
|
+
quality assurance team. &ldquo;As an unconfirmed user, I cannot message anyone,&rdquo; is a
|
407
|
+
quick example, but they can get bigger and more descriptive.</p>
|
408
|
+
<h2 id="retrospectives">
|
409
|
+
<a href="#retrospectives">
|
410
|
+
Retrospectives
|
411
|
+
</a>
|
412
|
+
</h2>
|
413
|
+
|
414
|
+
<p>Retrospectives is one way to attack the following problems:</p>
|
415
|
+
|
416
|
+
<ul>
|
417
|
+
<li>Unresolved conflict: the team is unable to communicate effectively during
|
418
|
+
conflict. Anger and resentment grow instead, buried and festering inside,
|
419
|
+
manifesting as passive-aggressive code review comments, poor collaboration,
|
420
|
+
low morale, people quitting, or an explosion of anger.</li>
|
421
|
+
<li>Fights: instead of passive-aggressive, low-level frustration, the team could
|
422
|
+
express constant anger. Code review ends in tears.</li>
|
423
|
+
</ul>
|
424
|
+
|
425
|
+
<p>Scheduled, frequent, recurring retrospectives are a time for the team to
|
426
|
+
reflect on their happiness and internal relationship. These happen rain or
|
427
|
+
shine: it helps to practice communication in good times so that it becomes a
|
428
|
+
normal reflex in times of need. Some teams pair it with drinks at the end of a
|
429
|
+
workday; others do it mid-day as a normal part of the workday.</p>
|
430
|
+
<h2 id="standups">
|
431
|
+
<a href="#standups">
|
432
|
+
Standups
|
433
|
+
</a>
|
434
|
+
</h2>
|
435
|
+
|
436
|
+
<p>Standup is one way to attack the following problems:</p>
|
437
|
+
|
438
|
+
<ul>
|
439
|
+
<li>Isolation: people feel lonely working on remote teams or siloed projects.</li>
|
440
|
+
<li>Othering: people on different teams have inhuman expectations of anothers&rsquo;
|
441
|
+
team. This is expressed as increasing demands via faceless platforms like
|
442
|
+
<a href="https://medium.com/@chrisjbatts/actually-slack-really-sucks-625802f1420a#.qf3n7xjdn">chat</a> and ticket trackers, constant rejection of solutions, or warring
|
443
|
+
teams.</li>
|
444
|
+
</ul>
|
445
|
+
|
446
|
+
<p>A quick check-in once a day to catch everyone up on small details: what issue
|
447
|
+
or project people are working on, whether they are blocked, perhaps something
|
448
|
+
interesting that they learned, and general announcements. They are done
|
449
|
+
standing in an effort to <a href="https://www.researchgate.net/publication/232529574_The_effects_of_stand-up_and_sit-down_meeting_formats_on_meeting_outcomes">keep them short</a> (more fit teams may want to do them
|
450
|
+
standing on one leg instead).</p>
|
451
|
+
<h2 id="ticket-tracker">
|
452
|
+
<a href="#ticket-tracker">
|
453
|
+
Ticket Tracker
|
454
|
+
</a>
|
455
|
+
</h2>
|
456
|
+
|
457
|
+
<p>A ticket tracker is one way to attack the following problems:</p>
|
458
|
+
|
459
|
+
<ul>
|
460
|
+
<li>Duplicated work: multiple people open code reviews, only to find that another
|
461
|
+
review exists with that exact feature implemented. This kind of simple
|
462
|
+
miscommunication can be exacerbated by large teams, <a href="http://basho.com/posts/technical/microservices-please-dont/">microservices</a>,
|
463
|
+
distributed teams, and other communication challenges.</li>
|
464
|
+
<li>Hurry up and wait: the marketing department waits until the feature is
|
465
|
+
shipped, and then hurries to advertise it (meanwhile they sat around
|
466
|
+
waiting).</li>
|
467
|
+
<li>Surprise changes: the support team first learns that the UI has changed when
|
468
|
+
complaints roll in about the redesign; the CEO hears about the removal of her
|
469
|
+
favorite feature during a Q&amp;A session at a conference.</li>
|
470
|
+
</ul>
|
471
|
+
|
472
|
+
<p>Ticket trackers are common, though often using different vocabulary. Trello
|
473
|
+
uses cards; Trajectory uses stories; Pivotal Tracker uses bugs, chores, and
|
474
|
+
stories. Jira does all of that plus provides visibility into metrics &ndash; some
|
475
|
+
of which are projections, and others that are correct.</p>
|
476
|
+
<h2 id="story-or-jobs-to-be-done-format">
|
477
|
+
<a href="#story-or-jobs-to-be-done-format">
|
478
|
+
Story or Jobs-To-Be-Done Format
|
479
|
+
</a>
|
480
|
+
</h2>
|
481
|
+
|
482
|
+
<p>Story format is one way to attack the following problems:</p>
|
483
|
+
|
484
|
+
<ul>
|
485
|
+
<li>Mysterious business: the developer will happily implement a feature, lacking
|
486
|
+
the understanding of how it fits into the product or how it might be used.
|
487
|
+
Long term this leads to a disconnect between the code and the product &ndash; the
|
488
|
+
domain-specific wording and language used throughout the app bears no
|
489
|
+
relation to the reality that it must model &ndash; causing frustration among the
|
490
|
+
users and confusion when onboarding new developers.</li>
|
491
|
+
<li>Unfollow-through: the developer implements the letter of the ticket, but not
|
492
|
+
the spirit, leading to situations where the feature is done but nothing
|
493
|
+
links to it; the JSON API functions but sends useless data; the user can
|
494
|
+
receive the password reset email but Gmail marks it as spam.</li>
|
495
|
+
<li>Inflexibility: when the dev runs into complications, she pushes through
|
496
|
+
instead of re-evaluating for time sensitivity. This one solution is the only
|
497
|
+
one, and no compromises are entertained, regardless of how long this takes
|
498
|
+
and how it affects the user or the company.</li>
|
499
|
+
</ul>
|
500
|
+
|
501
|
+
<p>The story format phrases tasks in terms that the end user cares about, with an
|
502
|
+
explanation for <a href="http://www.toyota-global.com/company/toyota_traditions/quality/mar_apr_2006.html">why</a> the user might want the task done; similarly, the <a href="https://robots.thoughtbot.com/converting-to-jobs-stories">jobs
|
503
|
+
story format</a> puts the user&rsquo;s context first and the <a href="http://www.kitchensoap.com/2014/11/14/the-infinite-hows-or-the-dangers-of-the-five-whys/">motivation</a> right in the
|
504
|
+
middle. This is all in contrast to the traditional task format, that focuses
|
505
|
+
only on what the developer must change in the code, with no explanation or
|
506
|
+
motivation.</p>
|
507
|
+
<h2 id="test-driven-design">
|
508
|
+
<a href="#test-driven-design">
|
509
|
+
Test-Driven Design
|
510
|
+
</a>
|
511
|
+
</h2>
|
512
|
+
|
513
|
+
<p>TDD is one way to attack the following problems:</p>
|
514
|
+
|
515
|
+
<ul>
|
516
|
+
<li>Blind fixes: the bug is fixed &hellip; or is it? No one is quite sure, but
|
517
|
+
the new commit sure has a great description of how it could have fixed the
|
518
|
+
bug.</li>
|
519
|
+
<li>Runtime whimsy: return values go unchecked, from <a href="https://robots.thoughtbot.com/why-do-rubyists-test-so-completely"><code>nil</code></a> to missing
|
520
|
+
files to failed credit card payments, leading to errors at runtime.</li>
|
521
|
+
<li>Fear of deployment: the development workflow is running efficiently until it
|
522
|
+
comes time to actually merge the branch. Hesitation, followed by asking for
|
523
|
+
review after review, followed by begging others for reviews because no one
|
524
|
+
wants the responsibility of saying that it will work in production.</li>
|
525
|
+
</ul>
|
526
|
+
|
527
|
+
<p>Test-driven design (TDD) uses programmatic tests to drive the design and
|
528
|
+
architecture of the codebase. An incidental side effect is that the major
|
529
|
+
codepaths are tested, including error flows. Running the test suite exercises
|
530
|
+
all parts of the application, finding regressions in paths where bugs have been
|
531
|
+
found and fixed.</p>
|
532
|
+
<h2 id="refactoring">
|
533
|
+
<a href="#refactoring">
|
534
|
+
Refactoring
|
535
|
+
</a>
|
536
|
+
</h2>
|
537
|
+
|
538
|
+
<p>Refactoring is one way to attack the following problems:</p>
|
539
|
+
|
540
|
+
<ul>
|
541
|
+
<li>Unworkable feature: adding the new feature requires its own separate app or a
|
542
|
+
fragile connection through the database. What could be a simple button on a
|
543
|
+
Web page that performs a common activity takes days, weeks, or months to
|
544
|
+
implement.</li>
|
545
|
+
<li>Hidden bug: you&rsquo;ve traced the crash to one method, but that method was
|
546
|
+
written by a developer from two generations of coworkers ago, is 127
|
547
|
+
lines long, and the commit message was &ldquo;it works&rdquo;. The twenty code paths,
|
548
|
+
including liberal use of <a href="https://docs.racket-lang.org/reference/cont.html#%28def._%28%28quote._%7E23%7E25kernel%29._call-with-escape-continuation%29%29"><code>return</code></a>, obscure the source of the error.</li>
|
549
|
+
</ul>
|
550
|
+
|
551
|
+
<p>Refactoring is the process of shuffling code around without adding any features
|
552
|
+
or fixing any bugs. It is often the first step to implementing a new feature or
|
553
|
+
bug fix, carving a more clear path through the system, as part of the
|
554
|
+
<a href="http://www.virtuouscode.com/2012/06/25/every-day-in-every-way/">red-green-refactor</a> workflow.</p>
|
555
|
+
<h2 id="feature-flags">
|
556
|
+
<a href="#feature-flags">
|
557
|
+
Feature Flags
|
558
|
+
</a>
|
559
|
+
</h2>
|
560
|
+
|
561
|
+
<p>Feature flags is one way to attack the following problems:</p>
|
562
|
+
|
563
|
+
<ul>
|
564
|
+
<li>Market timing: the feature is implemented but the rest of the company still
|
565
|
+
isn&rsquo;t ready for it. The support team needs to be trained on it, the
|
566
|
+
promotional announcement needs to be sent out, or the CEO needs to be
|
567
|
+
convinced that it&rsquo;s a good idea.</li>
|
568
|
+
<li>Questionable code: an isolated chunk of code &ndash; a new file system, for
|
569
|
+
example &ndash; is ready to be evaluated by willing and able participants, but
|
570
|
+
is not ready for public consumption until all the initial bugs have been
|
571
|
+
shaken out.</li>
|
572
|
+
</ul>
|
573
|
+
|
574
|
+
<p>Feature flags refers to hiding parts of the app behind a toggle, only shown to
|
575
|
+
some specific users or only enabled by an admin. These feature flags typically
|
576
|
+
differ from A/B testing in that they&rsquo;re less about measurement and more about
|
577
|
+
hiding.</p>
|
578
|
+
<h2 id="code-review">
|
579
|
+
<a href="#code-review">
|
580
|
+
Code Review
|
581
|
+
</a>
|
582
|
+
</h2>
|
583
|
+
|
584
|
+
<p>Code review is one way to attack the following problems:</p>
|
585
|
+
|
586
|
+
<ul>
|
587
|
+
<li>Siloed development: developers work in isolation on specific categories of
|
588
|
+
projects &ndash; one person on payment, another on API, a third on advertisement
|
589
|
+
targeting &ndash; but on the same codebase. Features change around them and cruft
|
590
|
+
grows without any clear communication channel between devs.</li>
|
591
|
+
<li>Poor code quality: the developers learn from blog posts and Web search
|
592
|
+
results instead of from each other, furthering their isolation. Coding styles
|
593
|
+
vary, and the same solution exists <a href="http://lists.llvm.org/pipermail/llvm-dev/2016-October/106079.html">multiple times</a> in the same repo.</li>
|
594
|
+
</ul>
|
595
|
+
|
596
|
+
<p>Reviewing code is the <a href="http://www.geraldmweinberg.com/Site/Programming_Psychology.html">process</a> of reading a diff: comparing a new commit with
|
597
|
+
what exists in the system. Often there is a focus on maintainability,
|
598
|
+
consistency, or knowledge transfer. Since it typically works on a diff, there
|
599
|
+
are fewer considerations for big picture harmony.</p>
|
600
|
+
<h2 id="git-workflow">
|
601
|
+
<a href="#git-workflow">
|
602
|
+
Git Workflow
|
603
|
+
</a>
|
604
|
+
</h2>
|
605
|
+
|
606
|
+
<p>A consistent git workflow is one way to attack the following problems:</p>
|
607
|
+
|
608
|
+
<ul>
|
609
|
+
<li>Unexplainable code: you find a strange line of code, the test explains
|
610
|
+
nothing, the commit message only says &ldquo;initial commit&rdquo;, the ticket tracker
|
611
|
+
was replaced twice since the project started, as was the project manager. Why
|
612
|
+
is this solution the right one, and why does the mysterious test enforce it?</li>
|
613
|
+
<li>Bus factor one: the developer works alone, deploys a JAR, and leaves no
|
614
|
+
comments. Then she quits and the new dev is onboarded. I hope you enjoyed
|
615
|
+
this short horror story.</li>
|
616
|
+
</ul>
|
617
|
+
|
618
|
+
<p>Git provides enough plumbing to hang yourself. Maybe that&rsquo;s not the expression.
|
619
|
+
Regardless, there&rsquo;s no one way to use git, and multiple right ways. From branch
|
620
|
+
names to merge strategies to <a href="https://robots.thoughtbot.com/5-useful-tips-for-a-better-commit-message">commit message</a> content, it&rsquo;s possible to have
|
621
|
+
<a href="https://github.com/thoughtbot/guides/tree/master/protocol/git">a unified version control process</a>.</p>
|
622
|
+
<h2 id="deployment-process">
|
623
|
+
<a href="#deployment-process">
|
624
|
+
Deployment Process
|
625
|
+
</a>
|
626
|
+
</h2>
|
627
|
+
|
628
|
+
<p>A deploy process is one way to attack the following problems:</p>
|
629
|
+
|
630
|
+
<ul>
|
631
|
+
<li>Expensive downtime: each second of downtime costs more in lost sales than
|
632
|
+
each second of development time costs in salary.</li>
|
633
|
+
<li>External failures: the deployment depends on a set of external services
|
634
|
+
providing DNS, caching and content delivery, uptime monitoring, error
|
635
|
+
reporting, and so on, each of which has their own failure modes.</li>
|
636
|
+
<li>Regulations: strict compliance with the law requires that very few people
|
637
|
+
have access to the database or the production servers.</li>
|
638
|
+
</ul>
|
639
|
+
|
640
|
+
<p>Going from development to production takes a few steps, which means it can be
|
641
|
+
scripted. The runnable script can live near a human-readable script, separately
|
642
|
+
describing who can deploy, what steps to take when it fails, what to do about
|
643
|
+
downtime, and how to announce it. Combined, the program and documentation
|
644
|
+
around it make up the deployment process.</p>
|
645
|
+
<h2 id="horizontal-scaling">
|
646
|
+
<a href="#horizontal-scaling">
|
647
|
+
Horizontal Scaling
|
648
|
+
</a>
|
649
|
+
</h2>
|
650
|
+
|
651
|
+
<p>Scaling is one way to attack the following problems:</p>
|
652
|
+
|
653
|
+
<ul>
|
654
|
+
<li>Concurrent users: the service is immediately more popular and needs to handle
|
655
|
+
twice as many users as before. They don&rsquo;t necessarily need to use the full
|
656
|
+
service, but at least need to have enough working to get their job done.</li>
|
657
|
+
<li>Concurrent processing: the algorithm can be subdivided into smaller,
|
658
|
+
independent problems, but each problem would take its own computer to solve.</li>
|
659
|
+
</ul>
|
660
|
+
|
661
|
+
<p>Scaling horizontally presents as spinning up more servers to handle the load;
|
662
|
+
this is in comparison with vertical scaling, where the CPU is sped up or the
|
663
|
+
RAM increased. The new servers come up quickly (&ldquo;instantly&rdquo;), run the same
|
664
|
+
software, and can be reduced when needed.</p>
|
665
|
+
<h2 id="and-more">
|
666
|
+
<a href="#and-more">
|
667
|
+
And more
|
668
|
+
</a>
|
669
|
+
</h2>
|
670
|
+
|
671
|
+
<p>This is just a tiny selection of the solutions that we have seen to real
|
672
|
+
problems over the years. Some are for fancy ideas &ndash; feature flags and story
|
673
|
+
format are not common to every team &ndash; and some are sacred cows, like TDD and
|
674
|
+
refactoring.</p>
|
675
|
+
|
676
|
+
<p>Your homework, dear reader, is to think of what problems you are solving and
|
677
|
+
what solutions are not carrying their weight. Let us know what you learn while
|
678
|
+
reflecting; we are always looking for <a href="https://thoughtbot.com/playbook">processes we can drop</a>!</p>
|
679
|
+
</content>
|
680
|
+
<summary>Avoid applying solutions in a vacuum.</summary>
|
681
|
+
<feedburner:origLink>https://robots.thoughtbot.com/you-aint-gonna-need-process</feedburner:origLink></entry>
|
682
|
+
</feed>
|