Slide 1

Slide 1 text

Droidcon Italy 2015 @rejasupotaro Kentaro Takiguchi Improving UX through performance

Slide 2

Slide 2 text

Tokyo is only 15 hours away!

Slide 3

Slide 3 text

Ruby is developed by Matz in Japan

Slide 4

Slide 4 text

Cookpad is a recipe sharing service written in RoR

Slide 5

Slide 5 text

2 million recipes 50 million UU / month 20 million downloads

Slide 6

Slide 6 text

https://speakerdeck.com/a_matsuda/the-recipe-for-the-worlds-largest-rails-monolith

Slide 7

Slide 7 text

Cookpad is expanding our businesses to new markets

Slide 8

Slide 8 text

Emerging market is leading smartphone growth

Slide 9

Slide 9 text

I was in Indonesia for a month to experience actual life in Indonesia

Slide 10

Slide 10 text

Not everyone is on a fast phone Not everyone is on a fast network

Slide 11

Slide 11 text

• Low bandwidth • Low spec devices The greatest challenges

Slide 12

Slide 12 text

… Connection speed in Indonesia is 5x slower than in Japan http://en.wikipedia.org/wiki/List_of_countries_by_Internet_connection_speeds

Slide 13

Slide 13 text

Performance is a Feature It is becoming increasingly important for mobile engineers to guarantee stable service under any environment

Slide 14

Slide 14 text

I’m rebuilding the Android app for new markets

Slide 15

Slide 15 text

• Efficient HTTP communication • Image optimization • API design Agenda

Slide 16

Slide 16 text

Efficient HTTP communication

Slide 17

Slide 17 text

Nginx Ruby on Rails ElastiCache HTTP Client

Slide 18

Slide 18 text

ElastiCache Nginx Ruby on Rails HTTP Client Stetho

Slide 19

Slide 19 text

A debug bridge for Android applications https://github.com/facebook/stetho

Slide 20

Slide 20 text

We can see network

Slide 21

Slide 21 text

We can see view hierarchy

Slide 22

Slide 22 text

We can access SQLite database

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

Compressing Data An easy and convenient way to reduce the bandwidth

Slide 25

Slide 25 text

90% GZIP reduce the size of response Compression is a simple, effective way

Slide 26

Slide 26 text

Nginx Ruby on Rails Memcached Rack::Cache HTTP Client Stetho How do we compress data?

Slide 27

Slide 27 text

Nginx Rails HTTP Client Accept-Encoding: gzip Content-Encoding: gzip

Slide 28

Slide 28 text

http { ... gzip on; gzip_disable "msie6"; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_types text/plain text/css application/json } nginx.conf Nginx Rails

Slide 29

Slide 29 text

HTTP Client GZIP decoder // Set "Accept-Encoding: gzip" when you send a request connection.setRequestProperty( "Accept-Encoding", “gzip"); // Decompress input stream when you receive a response inputStream = new GZIPInputStream( connection.getInputStream());

Slide 30

Slide 30 text

• AndroidHttpClient • HttpUrlConnection • OkHttp HTTP clients for Android Don’t support GZIP by default support GZIP by default

Slide 31

Slide 31 text

• AndroidHttpClient • HttpUrlConnection • OkHttp HTTP clients for Android @Deprecated No longer maintained

Slide 32

Slide 32 text

We had used Volley as API client before

Slide 33

Slide 33 text

Volley has 2 HTTP clients internally public static RequestQueue newRequestQueue(…) { ... if (stack == null) { if (Build.VERSION.SDK_INT >= 9) { // use HttpUrlConnection stack = new HurlStack(); } else { // use AndroidHttpClient stack = new HttpClientStack(AndroidHttpClie } } 2.3+: HttpUrlConnection <2.2: AndroidHttpClient Volley

Slide 34

Slide 34 text

4.4+: OkHttp <4.4: HttpUrlConnection HttpUrlConnection HttpUrlConnection uses OkHttp internally

Slide 35

Slide 35 text

4.4+: OkHttp <4.4: HttpUrlConnection Different behavior of HTTP clients <2.3: AndroidHttpClient Inside of Volley

Slide 36

Slide 36 text

Simple is better

Slide 37

Slide 37 text

I recommend to use OkHttp * GZIP * Connection Pool * WebSocket * HTTP/2.0

Slide 38

Slide 38 text

OkHttp + RxJava = Reactive Data Store View Adapter Service API Client Server OkHttp RxJava SQLite Database SharedPreferences

Slide 39

Slide 39 text

Caching Data Effective cache controls will dramatically reduce server load

Slide 40

Slide 40 text

OkHttp Disk Cache OkHttp core

Slide 41

Slide 41 text

Caching in HTTP cache-request-directive = "no-cache" | "no-store" | "max-age" "=" delta-seconds | "max-stale" [ "=" delta-seconds ] | "min-fresh" "=" delta-seconds | "no-transform" | "only-if-cached" | cache-extension cache-response-directive = "public" | "private" [ "=" <"> 1#field-name <"> ] | "no-cache" [ "=" <"> 1#field-name <"> ] | "no-store" | "no-transform" | "must-revalidate" | "proxy-revalidate" | "max-age" "=" delta-seconds | "s-maxage" "=" delta-seconds | cache-extension

Slide 42

Slide 42 text

OkHttpClient client = new OkHttpClient(); Cache cache = new Cache(cacheDir, MAX_CACHE_SIZE); client.setCache(cache); Enable cache

Slide 43

Slide 43 text

# default # => Cache-Control: max-age=0, private, must-revalidate expires_in(1.hour, public: true) # => Cache-Control: max-age=3600, public expires_now # => Cache-Control: no-cache Rails

Slide 44

Slide 44 text

OkHttp core GET /recipes Response Cache Response key GET /recipes Response

Slide 45

Slide 45 text

OkHttp core Cache Response key GET /recipes Response = urlToKey(request)

Slide 46

Slide 46 text

OkHttp core Cache PUT /recipes/:id Response PUT /recipes/:id Response Response key = urlToKey(request)

Slide 47

Slide 47 text

In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip the cache, and fetch data directly from the server Cache-Control: no-cache

Slide 48

Slide 48 text

// RecipeService.java public Observable> get(…) { ... return request(GET, “/recipes/:id”) .noCache() .noStore() .to(RECIPE); } // ApiClient.java if (isConnected) { headers.put(CACHE_CONTROL, “only-if-cached"); } else if (noCache && noStore) { headers.put(CACHE_CONTROL, "no-cache, no-store"); } else if (noCache) { headers.put(CACHE_CONTROL, "no-cache"); } else if (noStore) { headers.put(CACHE_CONTROL, "no-store"); } HttpRequestCreator RecipeService ApiClient

Slide 49

Slide 49 text

Users can see contents quickly even if device is not connected

Slide 50

Slide 50 text

Object Type Duration Categories 1 day Search recipes 3 hours Users Do not cache To enjoy the benefits of caching, you need to write carefully crafted cache control policies

Slide 51

Slide 51 text

Image Optimization

Slide 52

Slide 52 text

Image size is much larger than JSON response {"result":{"id":1,"title":"Penne with Spring Vegetables”,”description”:”..." Each pixel takes up 4 bytes

Slide 53

Slide 53 text

We need to know what image loading is

Slide 54

Slide 54 text

• Specify URL to HTTP client • Get Input Steam • Decode Input Stream to Bitmap • Set Bitmap to ImageView Simple Image Loading

Slide 55

Slide 55 text

? Do you fetch images from the server every time you want to display images?

Slide 56

Slide 56 text

The answer may be “NO”

Slide 57

Slide 57 text

In addition, we want to • reuse worker threads • set the priority of requests • cache decoded images

Slide 58

Slide 58 text

Fresco Picasso There are some great libraries

Slide 59

Slide 59 text

Caching Data The best way to display images quickly

Slide 60

Slide 60 text

OkHttp core Picasso Disk Cache Memory Cache

Slide 61

Slide 61 text

Expiration times of cache is also following cache controls Expiration time

Slide 62

Slide 62 text

Picasso setup cache automatically You don’t need to do anything Enable cache

Slide 63

Slide 63 text

Thread Pool Creating new threads for each task incur the overhead

Slide 64

Slide 64 text

Main Thread Worker Thread Request Image CloudFront • Transform • Decode • Cache Worker Thread Worker Thread

Slide 65

Slide 65 text

new ThreadPoolExecutor( corePoolSize, // The number of threads to keep in the pool maximumPoolSize, // The maximum number of threads to allow in the pool keepAliveTime, // the maximum time that excess idle threads will wait for new tasks timeUnit, // for the keepAliveTime argument workQueue, // the queue to use for holding tasks before they are executed threadFactory // The factory to use when the executor creates a new thread ); Task Result

Slide 66

Slide 66 text

Producer-consumer pattern Send a request from main thread Control order of requests Receive a request through channel. Send result through Hander.

Slide 67

Slide 67 text

There is a trade-off between capacity and resource If there are many workers, tasks are processed concurrently. If there are too many workers, consume memory wastefully.

Slide 68

Slide 68 text

switch (info.getType()) { case ConnectivityManager.TYPE_WIFI: case ConnectivityManager.TYPE_WIMAX: case ConnectivityManager.TYPE_ETHERNET: setThreadCount(4); break; case ConnectivityManager.TYPE_MOBILE: switch (info.getSubtype()) { case TelephonyManager.NETWORK_TYPE_LTE: // 4G case TelephonyManager.NETWORK_TYPE_HSPAP: case TelephonyManager.NETWORK_TYPE_EHRPD: setThreadCount(3); break; case TelephonyManager.NETWORK_TYPE_UMTS: // 3G case TelephonyManager.NETWORK_TYPE_CDMA: case TelephonyManager.NETWORK_TYPE_EVDO_0: case TelephonyManager.NETWORK_TYPE_EVDO_A: case TelephonyManager.NETWORK_TYPE_EVDO_B: setThreadCount(2); break; case TelephonyManager.NETWORK_TYPE_GPRS: // 2G case TelephonyManager.NETWORK_TYPE_EDGE: setThreadCount(1); break; Runtime.getRuntime().availableProcessors() Picasso Glide

Slide 69

Slide 69 text

Which setting is better? It is depending on network environment, device spec, image size, transformation, …

Slide 70

Slide 70 text

Fresco A new image loading library developed by Facebook

Slide 71

Slide 71 text

NUM_IO_BOUND_THREADS = 2; NUM_CPU_BOUND_THREADS = Runtime.getRuntime().availableProcessors(); Process Kind of Executor forLocalStorageRead IoBoundExecutor forLocalStorageWrite IoBoundExecutor forDecode CpuBoundExecutor forBackground CpuBoundExecutor Fresco has multiple Executors

Slide 72

Slide 72 text

Queue Management Control order of requests

Slide 73

Slide 73 text

PriorityBlockingQueue The elements order themselves according to whatever priority you decided in your Comparable implementation

Slide 74

Slide 74 text

We can set priority to request Picasso.with(this) .load(url) .priority(HIGH) .into(imageView); Glide.with(this) .load(url) .priority(HIGH) .into(imageView);

Slide 75

Slide 75 text

)PXQSJPSJUZXPSLT 8IFOBVTFSPQFOSFDJQFEFUBJMTDSFFO SFRVFTUTBSFBEEFEUPUIFFOEPGUIFRVFVF

Slide 76

Slide 76 text

)PXQSJPSJUZXPSLT 8IFOUIFVTFSPQFOSFDJQFEFUBJMTDSFFO TFU)*()QSJPSJUZUPUIFNBJOJNBHF )*() )*()

Slide 77

Slide 77 text

)PXQSJPSJUZXPSLT XIFOUIFVTFSCBDLUPSFDJQFMJTUTDSFFO DBMMlDBODFM5BHzUPEJTQPTFVTFMFTTSFRVFTUT

Slide 78

Slide 78 text

(MJEFIBTMJGFDZDMFJOUFHSBUJPO notify lifecycle events

Slide 79

Slide 79 text

3FRVFTUTJOTFBSDISFTVMUTDSFFO BSFQBVTFEBVUPNBUJDBMMZ (MJEFNBOBHFUIFRVFVFBVUPNBUJDBMMZ

Slide 80

Slide 80 text

3FRVFTUTJOTFBSDISFDJQFMJTU JTSFTUBSUFEBVUPNBUJDBMMZ (MJEFNBOBHFUIFRVFVFBVUPNBUJDBMMZ 3FRVFTUTJOSFDJQFEFUBJMTDSFFO BSFDBODFMMFEBVUPNBUJDBMMZ

Slide 81

Slide 81 text

/PUJDF (MJEFBEETWJFXMFTTGSBHNFOUUPFBDI"DUJWJUZ UPPCTFSWFMJGFDZDMFFWFOUT

Slide 82

Slide 82 text

Bitmap Pool Reuse memory when new Bitmap is requested

Slide 83

Slide 83 text

&BDIQJYFMUBLFTVQCZUFT '''% QYQYCZUF CZUF Memory management for Bitmap

Slide 84

Slide 84 text

XJEUI IFJHIU DPOpH #JUNBQ 3FRVFTUB#JUNBQ (MJEFIBT#JUNBQ1PPM 4J[F4USBUFHZ "UUSJCVUF4USBUFHZ reuse resources to avoid unnecessary allocations

Slide 85

Slide 85 text

4J[F4USBUFHZ "UUSJCVUF4USBUFHZ

Slide 86

Slide 86 text

Image Format We are using WebP that is an image format developed by Google

Slide 87

Slide 87 text

WebP lossless images are 26% smaller in size compared to PNGs WebP lossy images are 25-34% smaller in size compared to JPEGs

Slide 88

Slide 88 text

Comparison of image size jpeg webp (q = 90) webp (q = 70) webp (q = 50) webp (q = 80) webp (q = 60) 74% 90,602 bytes 30,214 bytes 23,550 bytes 20,882 bytes 18,344 bytes 51,288 bytes

Slide 89

Slide 89 text

Image Size Request an appropriate image size

Slide 90

Slide 90 text

Nexus 5 Nexus S Nexus 9

Slide 91

Slide 91 text

target.getWidth() => 1080 http://.../1080x756/photo.webp target.getHeight() => 756

Slide 92

Slide 92 text

We are using image transformation server called Tofu. Tofu transforms images on the fly.

Slide 93

Slide 93 text

Tofu Decoder S3 https://... 101001010101… CloudFront (Transformation) (Cache) • Fixed Width: (\d+) • Fixed Height: x(\d+) • Fixed Width and Height: (\d+)x(\d+) • Smaller than: (\d+)?(x\d+)?s • Cropping: (\d+)x(\d+)c • Manual Cropping: (\d+)x(\d+)c(\d+)_(\d+)_(\d+)_(\d+)_(\d+) • Quality Factor: [geometry]q(\d+) • … Tofu has these functions

Slide 94

Slide 94 text

Request different image size depends on network quality Picasso ConnectivityObserver ImageLoader ImageRequestCreator

Slide 95

Slide 95 text

EXCELLENT: (1080 * 756) * 1.0 LOW: (756 * 530) * 0.7 86KB 49KB LOW images are 40% smaller than full images

Slide 96

Slide 96 text

API Design

Slide 97

Slide 97 text

If API responses become faster, users become happier. ?

Slide 98

Slide 98 text

Of course, the answer is “Yes”

Slide 99

Slide 99 text

Let’s use partial response to reduce data size

Slide 100

Slide 100 text

But be careful, Android has state and screen transition

Slide 101

Slide 101 text

Users go back and forth to decide a recipe

Slide 102

Slide 102 text

Thing we have to do is Optimizing UX > response time

Slide 103

Slide 103 text

… 10,000 ms 200 ms or below Distance between phone and server is very very very … long Particularly in emerging markets

Slide 104

Slide 104 text

Reduce unnecessary fields Get necessary relations GOOD Bad

Slide 105

Slide 105 text

One more thing to improve experience

Slide 106

Slide 106 text

0.4KB { “id":1, "title":"Penne with Spring Vegetables”, “thumbnail_data_uri": “data:image/jpeg;base64,/9j/4AAQSkZJRg…”, “description”: “…” } 10px 10px Response include thumbnail_data_uri Base64 encoded image

Slide 107

Slide 107 text

Data size is small but there is a big improvement

Slide 108

Slide 108 text

Documentation

Slide 109

Slide 109 text

Keeping the documentation updated in real time is hard

Slide 110

Slide 110 text

We are working on separated timezone

Slide 111

Slide 111 text

Hi, can I ask you a question about API? Today … Sorry for late reply

Slide 112

Slide 112 text

JSON Schema We are using as the format for describing our APIs

Slide 113

Slide 113 text

JSON Schema provides • Request Validation • Response Validation • Document generation

Slide 114

Slide 114 text

RequestValidation ResponseValidation Check request/response automatically

Slide 115

Slide 115 text

Generate API documentation from schema file

Slide 116

Slide 116 text

We don’t need to update documentation manually. And we can see latest documentation any time.

Slide 117

Slide 117 text

Conclusion

Slide 118

Slide 118 text

GZIP Cache Controls Stetho Base64 encoded thumbnail Partial response Appropriate data model Stetho WebP Prioritized request Appropriate image size Generate documentation Auto validation App server Image server

Slide 119

Slide 119 text

@rejasupotaro Kentaro Takiguchi Thank you!