feedjira 2.1.0 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|