In this example, we are going to apply a CNN to classify dogs vs. cats images. This will walk you through the fundamentals of importing images, applying image augmentation, and performing classification on them.

Learning objectives:

  • What image generators are, why and how to use them.
  • What image augmentation is, why and how to use them.

Required packages

library(keras)
library(glue)
library(tidyverse)

Data Preparation

Image location

We are going to use the Dogs vs. Cats Kaggle competition data set (https://www.kaggle.com/c/dogs-vs-cats/data). However, do to size and runtime limitations, we are going to only use a subset of the data. We have already set up the directories which look like:

- data
   └── dogs-vs-cats
       └── train
           └── cats
               ├── cat.1.jpg
               ├── cat.2.jpg
               └── ...
           └── dogs
               ├── dog.1.jpg
               ├── dog.2.jpg
               └── ...
       └── validation
           ├── cats
           └── dogs
       └── test
           ├── cats
           └── dogs
# define the directories:
if (stringr::str_detect(here::here(), "conf-2020-user")) {
  image_dir <- "/home/conf-2020-user/data/dogs-vs-cats"
} else {
  image_dir <- here::here("materials", "data", "dogs-vs-cats")
}
train_dir <- file.path(image_dir, "train")
valid_dir <- file.path(image_dir, "validation")
test_dir <- file.path(image_dir, "test")

# create train, validation, and test file paths for cat images
train_cats_dir <- file.path(train_dir, "cats")
valid_cats_dir <- file.path(valid_dir, "cats")
test_cats_dir <- file.path(test_dir, "cats")

# create train, validation, and test file paths for dog images
train_dogs_dir <- file.path(train_dir, "dogs")
valid_dogs_dir <- file.path(valid_dir, "dogs")
test_dogs_dir <- file.path(test_dir, "dogs")

Data set

Although there are 25,000 images in this data set, we are going to use a very small subset, which includes:

glue("Cat images:",
     " - total training cat images: {length(list.files(train_cats_dir))}",
     " - total training cat images: {length(list.files(train_cats_dir))}",
     " - total test cat images: {length(list.files(test_cats_dir))}",
     "\n",
     "Dog images:",
     " - total training dog images: {length(list.files(train_dogs_dir))}",
     " - total validation dog images: {length(list.files(valid_dogs_dir))}",
     " - total test dog images: {length(list.files(test_dogs_dir))}",
     .sep = "\n"
     )
Cat images:
 - total training cat images: 1000
 - total training cat images: 1000
 - total test cat images: 500


Dog images:
 - total training dog images: 1000
 - total validation dog images: 500
 - total test dog images: 500

Let’s check out the first 10 cat and dog images:

op <- par(mfrow = c(4, 5), pty = "s", mar = c(0.1, 0.1, 0.1, 0.1))
for (i in 1:10) {
  plot(as.raster(jpeg::readJPEG(paste0(train_cats_dir, "/cat.", i, ".jpg"))))
  plot(as.raster(jpeg::readJPEG(paste0(train_dogs_dir, "/dog.", i, ".jpg"))))
}
par(op)

CNN with image generator

Define and compile model

We’re going to set up a simple CNN model that contains steps you saw in the previous module. This CNN includes:

  • Four sequential conv and max pooling layers
  • Flatten layer
  • Densly-connected network
  • Single binary output
model <- keras_model_sequential() %>%
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu", 
                input_shape = c(150, 150, 3)) %>%
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  
  layer_flatten() %>%
  
  layer_dense(units = 512, activation = "relu") %>%
  layer_dense(units = 1, activation = "sigmoid")

summary(model)
Model: "sequential_2"
___________________________________________________________________________________________________
Layer (type)                                Output Shape                            Param #        
===================================================================================================
conv2d_8 (Conv2D)                           (None, 148, 148, 32)                    896            
___________________________________________________________________________________________________
max_pooling2d_8 (MaxPooling2D)              (None, 74, 74, 32)                      0              
___________________________________________________________________________________________________
conv2d_9 (Conv2D)                           (None, 72, 72, 64)                      18496          
___________________________________________________________________________________________________
max_pooling2d_9 (MaxPooling2D)              (None, 36, 36, 64)                      0              
___________________________________________________________________________________________________
conv2d_10 (Conv2D)                          (None, 34, 34, 128)                     73856          
___________________________________________________________________________________________________
max_pooling2d_10 (MaxPooling2D)             (None, 17, 17, 128)                     0              
___________________________________________________________________________________________________
conv2d_11 (Conv2D)                          (None, 15, 15, 128)                     147584         
___________________________________________________________________________________________________
max_pooling2d_11 (MaxPooling2D)             (None, 7, 7, 128)                       0              
___________________________________________________________________________________________________
flatten_2 (Flatten)                         (None, 6272)                            0              
___________________________________________________________________________________________________
dense_4 (Dense)                             (None, 512)                             3211776        
___________________________________________________________________________________________________
dense_5 (Dense)                             (None, 1)                               513            
===================================================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
___________________________________________________________________________________________________

Compile the model:

model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 0.0001),
  metrics = "accuracy"
)

Read images from directories

Next, we need a process that imports our images and transforms them to tensors that our model can process. We’ll use two functions to perform this process.

image_data_generator will:

  1. Read the image files
  2. Decode the image to RGB grids of pixels
  3. Convert these into floating point tensors
  4. Rescale pixel values to [0, 1] interval

image_data_generator provides other capabilities that we’ll look at shortly.

flow_images_from_directory will:

  1. Apply image_data_generator
  2. To a batch of 20 images at a time
  3. From our training directory (randomly shuffling between subdirectories)
  4. Resize these images to be consistent size of 150x150 pixels
  5. Apply binary labels
train_datagen <- image_data_generator(rescale = 1/255)
valid_datagen <- image_data_generator(rescale = 1/255)

train_generator <- flow_images_from_directory(
  train_dir,
  train_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

validation_generator <- flow_images_from_directory(
  valid_dir,
  valid_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

If we get the first batch from the generator, you will see that it yields 20 images of 150x150 pixels with three channels (20, 150, 150, 3) along with their binary labels (0, 1).

batch <- generator_next(train_generator)
str(batch)
List of 2
 $ : num [1:20, 1:150, 1:150, 1:3] 0.3765 0.4863 0.0196 0.2902 0.0471 ...
 $ : num [1:20(1d)] 0 0 0 1 1 0 0 1 1 0 ...

Train the model

To train our model we’ll use fit_generator which is the equivalent of fit for data generators. We provide it our generators for the training and validation data. Plus, we need to specify:

  • steps_per_epoch: how many samples to draw from the training generator before declaring an epoch over. Our generator supplies batches of 20 and we have 2,000 training images so we need 100 steps.
  • validation_steps: how many samples to draw from the validation generator. Our generator supplies batches of 20 and we have 1,000 validation images so we need 50 steps.

Note:

  • Without a GPU this will take approximately 20 minutes to train
  • With GPUs this will take approximately 5 minutes
history <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 100,
  epochs = 30,
  validation_data = validation_generator,
  validation_steps = 50,
  callbacks = callback_early_stopping(patience = 5)
)

Our first model’s performance is not that bad but definitely has room for improvement.

best_epoch <- which.min(history$metrics$val_loss)
best_loss <- history$metrics$val_loss[best_epoch] %>% round(3)
best_acc <- history$metrics$val_accuracy[best_epoch] %>% round(3)

glue("Our optimal loss is {best_loss} with an accuracy of {best_acc}")
Our optimal loss is 0.559 with an accuracy of 0.721
plot(history) + 
  scale_x_continuous(limits = c(0, length(history$metrics$val_loss)))

CNN with Image Augmentation

Image Augmentation

Our model above does ok but definitely has room for improvement. One approach to improve performance is to collect more data. Unfortunately, this is not always an option. An alternative is to use image augmentation. ℹ️

datagen <- image_data_generator(
  rescale = 1/255,
  rotation_range = 40,
  width_shift_range = 0.2,
  height_shift_range = 0.2,
  shear_range = 0.2,
  zoom_range = 0.2,
  horizontal_flip = TRUE,
  fill_mode = "nearest"
)

The following helps to visualize the idea of image augmentation by:

  • Reading in the first image and resizing it to 150x150,
  • Converting it to an array with shape (150, 150, 3),
  • Reshaping it to (1, 150, 150, 3),
  • Generating batches of randomly transformed images.
# get the first cat image
fnames <- list.files(train_cats_dir, full.names = TRUE)
img_path <- fnames[[1]]

# resize & reshape
img <- image_load(img_path, target_size = c(150, 150))
img_array <- image_to_array(img)
img_array <- array_reshape(img_array, c(1, 150, 150, 3))

# generate a a single augmented image
augmentation_generator <- flow_images_from_data(
  img_array,
  generator = datagen,
  batch_size = 1
)

# plot 10 augmented images of the first cat image
op <- par(mfrow = c(2, 5), pty = "s", mar = c(0, 0.1, 0, 0.1))
for (i in 1:10) {
  batch <- generator_next(augmentation_generator)
  plot(as.raster(batch[1,,,]))
}
par(op)

Build & train model

Let’s create a new model that includes image augmentation and we’ll apply the dropout regularization method. The following creates a CNN architecture with:

  • Four sequential conv and max pooling layers
  • Flatten layer
  • Dropout layer
  • Densly-connected network

All of which you are familiary with by now.

model <- keras_model_sequential() %>%
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu", input_shape = c(150, 150, 3)) %>%
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>% 
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  layer_flatten() %>%
  layer_dropout(rate = 0.5) %>%
  layer_dense(units = 512, activation = "relu") %>%
  layer_dense(units = 1, activation = "sigmoid")

model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 0.0001),
  metrics = "accuracy"
)

Now we can add image augmentation to our image_data_generator(). The rest of the inputs remain the same.

Note:

  • Without a GPU this will take approximately 60 minutes to train
  • With GPUs this will take approximately 20-30 minutes
# only augment training data
train_datagen <- image_data_generator(
  rescale = 1/255,
  rotation_range = 40,
  width_shift_range = 0.2,
  height_shift_range = 0.2,
  shear_range = 0.2,
  zoom_range = 0.2,
  horizontal_flip = TRUE,
)

# do not augment test and validation data
test_datagen <- image_data_generator(rescale = 1/255)

# generate batches of data from training directory
train_generator <- flow_images_from_directory(
  train_dir,
  train_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

# generate batches of data from validation directory
validation_generator <- flow_images_from_directory(
  valid_dir,
  test_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

# train model
history <- model %>%
  fit_generator(
    train_generator,
    steps_per_epoch = 100,
    epochs = 100,
    validation_data = validation_generator,
    validation_steps = 50,
    callbacks = callback_early_stopping(patience = 10)
  )

As you can see, using image augmentation helps to improve our model’s performance. In fact, if we had more patience with our early stopping we may even be able to nudge out a little more loss reduction.

best_epoch <- which.min(history$metrics$val_loss)
best_loss <- history$metrics$val_loss[best_epoch] %>% round(3)
best_acc <- history$metrics$val_accuracy[best_epoch] %>% round(3)

glue("Our optimal loss is {best_loss} with an accuracy of {best_acc}")
Our optimal loss is 0.439 with an accuracy of 0.795
plot(history) + 
  scale_x_continuous(limits = c(0, length(history$metrics$val_loss)))

Save the model

We can always save our models as h5 files. Let’s save this model as we will use it one of the “extras” notebooks to illustrate how we can visualize CNNs (see this notebook https://rstudio-conf-2020.github.io/dl-keras-tf/notebooks/visualizing-what-cnns-learn.nb.html).

model %>% save_model_hdf5("cats_and_dogs_small_1.h5")

However, we still have room for improvement because we are only using a small subset of the available data. We have two options to improve our model:

  1. Use more data. We are only using 2,000 of the 25,000 available images. However, this would have a significant impact on compute time.

  2. Use transfer learning. This is much quicker than the first option so in the next module I demonstrate how to use transfer learning for CNNs.

Takeways

  • When using image data we…
    • use image_data_generator to read the images, decode pixel values, convert to a tensor, rescale, and perform image augmentation.
    • use flow_images_from_directory import batches of our images, apply the image_data_generator, resize, and infer training labels.
  • Image augmentation such as zooming, flipping, rotating, shearing, etc. helps with image variance, provides free additional data, and generally improves model performance.

🏠

LS0tCnRpdGxlOiAiQ29tcHV0ZXIgdmlzaW9uICYgQ05OczogQ2F0cyB2cy4gRG9ncyIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB0cnVlCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0UpCmdncGxvdDI6OnRoZW1lX3NldChnZ3Bsb3QyOjp0aGVtZV9idygpKQpgYGAKCkluIHRoaXMgZXhhbXBsZSwgd2UgYXJlIGdvaW5nIHRvIGFwcGx5IGEgQ05OIHRvIGNsYXNzaWZ5IGRvZ3MgdnMuIGNhdHMgaW1hZ2VzLiAKVGhpcyB3aWxsIHdhbGsgeW91IHRocm91Z2ggdGhlIGZ1bmRhbWVudGFscyBvZiBpbXBvcnRpbmcgaW1hZ2VzLCBhcHBseWluZyBpbWFnZSAKYXVnbWVudGF0aW9uLCBhbmQgcGVyZm9ybWluZyBjbGFzc2lmaWNhdGlvbiBvbiB0aGVtLgoKTGVhcm5pbmcgb2JqZWN0aXZlczoKCi0gV2hhdCBpbWFnZSBnZW5lcmF0b3JzIGFyZSwgd2h5IGFuZCBob3cgdG8gdXNlIHRoZW0uCi0gV2hhdCBpbWFnZSBhdWdtZW50YXRpb24gaXMsIHdoeSBhbmQgaG93IHRvIHVzZSB0aGVtLgoKIyBSZXF1aXJlZCBwYWNrYWdlcwoKYGBge3J9CmxpYnJhcnkoa2VyYXMpCmxpYnJhcnkoZ2x1ZSkKbGlicmFyeSh0aWR5dmVyc2UpCmBgYAoKIyBEYXRhIFByZXBhcmF0aW9uCgojIyBJbWFnZSBsb2NhdGlvbgoKV2UgYXJlIGdvaW5nIHRvIHVzZSB0aGUgRG9ncyB2cy4gQ2F0cyBLYWdnbGUgY29tcGV0aXRpb24gZGF0YSBzZXQKKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vYy9kb2dzLXZzLWNhdHMvZGF0YSkuIEhvd2V2ZXIsIGRvIHRvIHNpemUgYW5kIHJ1bnRpbWUgCmxpbWl0YXRpb25zLCB3ZSBhcmUgZ29pbmcgdG8gb25seSB1c2UgYSBzdWJzZXQgb2YgdGhlIGRhdGEuICBXZSBoYXZlIGFscmVhZHkgCnNldCB1cCB0aGUgZGlyZWN0b3JpZXMgd2hpY2ggbG9vayBsaWtlOgoKCmBgYAotIGRhdGEKICAg4pSU4pSA4pSAIGRvZ3MtdnMtY2F0cwogICAgICAg4pSU4pSA4pSAIHRyYWluCiAgICAgICAgICAg4pSU4pSA4pSAIGNhdHMKICAgICAgICAgICAgICAg4pSc4pSA4pSAIGNhdC4xLmpwZwogICAgICAgICAgICAgICDilJzilIDilIAgY2F0LjIuanBnCiAgICAgICAgICAgICAgIOKUlOKUgOKUgCAuLi4KICAgICAgICAgICDilJTilIDilIAgZG9ncwogICAgICAgICAgICAgICDilJzilIDilIAgZG9nLjEuanBnCiAgICAgICAgICAgICAgIOKUnOKUgOKUgCBkb2cuMi5qcGcKICAgICAgICAgICAgICAg4pSU4pSA4pSAIC4uLgogICAgICAg4pSU4pSA4pSAIHZhbGlkYXRpb24KICAgICAgICAgICDilJzilIDilIAgY2F0cwogICAgICAgICAgIOKUlOKUgOKUgCBkb2dzCiAgICAgICDilJTilIDilIAgdGVzdAogICAgICAgICAgIOKUnOKUgOKUgCBjYXRzCiAgICAgICAgICAg4pSU4pSA4pSAIGRvZ3MKYGBgCgpgYGB7ciBpbWFnZS1maWxlLXBhdGhzfQojIGRlZmluZSB0aGUgZGlyZWN0b3JpZXM6CmlmIChzdHJpbmdyOjpzdHJfZGV0ZWN0KGhlcmU6OmhlcmUoKSwgImNvbmYtMjAyMC11c2VyIikpIHsKICBpbWFnZV9kaXIgPC0gIi9ob21lL2NvbmYtMjAyMC11c2VyL2RhdGEvZG9ncy12cy1jYXRzIgp9IGVsc2UgewogIGltYWdlX2RpciA8LSBoZXJlOjpoZXJlKCJtYXRlcmlhbHMiLCAiZGF0YSIsICJkb2dzLXZzLWNhdHMiKQp9CnRyYWluX2RpciA8LSBmaWxlLnBhdGgoaW1hZ2VfZGlyLCAidHJhaW4iKQp2YWxpZF9kaXIgPC0gZmlsZS5wYXRoKGltYWdlX2RpciwgInZhbGlkYXRpb24iKQp0ZXN0X2RpciA8LSBmaWxlLnBhdGgoaW1hZ2VfZGlyLCAidGVzdCIpCgojIGNyZWF0ZSB0cmFpbiwgdmFsaWRhdGlvbiwgYW5kIHRlc3QgZmlsZSBwYXRocyBmb3IgY2F0IGltYWdlcwp0cmFpbl9jYXRzX2RpciA8LSBmaWxlLnBhdGgodHJhaW5fZGlyLCAiY2F0cyIpCnZhbGlkX2NhdHNfZGlyIDwtIGZpbGUucGF0aCh2YWxpZF9kaXIsICJjYXRzIikKdGVzdF9jYXRzX2RpciA8LSBmaWxlLnBhdGgodGVzdF9kaXIsICJjYXRzIikKCiMgY3JlYXRlIHRyYWluLCB2YWxpZGF0aW9uLCBhbmQgdGVzdCBmaWxlIHBhdGhzIGZvciBkb2cgaW1hZ2VzCnRyYWluX2RvZ3NfZGlyIDwtIGZpbGUucGF0aCh0cmFpbl9kaXIsICJkb2dzIikKdmFsaWRfZG9nc19kaXIgPC0gZmlsZS5wYXRoKHZhbGlkX2RpciwgImRvZ3MiKQp0ZXN0X2RvZ3NfZGlyIDwtIGZpbGUucGF0aCh0ZXN0X2RpciwgImRvZ3MiKQpgYGAKCiMjIERhdGEgc2V0CgpBbHRob3VnaCB0aGVyZSBhcmUgMjUsMDAwIGltYWdlcyBpbiB0aGlzIGRhdGEgc2V0LCB3ZSBhcmUgZ29pbmcgdG8gdXNlIGEgdmVyeSAKc21hbGwgc3Vic2V0LCB3aGljaCBpbmNsdWRlczoKCmBgYHtyIHZlcmlmeS1kYXRhfQpnbHVlKCJDYXQgaW1hZ2VzOiIsCiAgICAgIiAtIHRvdGFsIHRyYWluaW5nIGNhdCBpbWFnZXM6IHtsZW5ndGgobGlzdC5maWxlcyh0cmFpbl9jYXRzX2RpcikpfSIsCiAgICAgIiAtIHRvdGFsIHRyYWluaW5nIGNhdCBpbWFnZXM6IHtsZW5ndGgobGlzdC5maWxlcyh0cmFpbl9jYXRzX2RpcikpfSIsCiAgICAgIiAtIHRvdGFsIHRlc3QgY2F0IGltYWdlczoge2xlbmd0aChsaXN0LmZpbGVzKHRlc3RfY2F0c19kaXIpKX0iLAogICAgICJcbiIsCiAgICAgIkRvZyBpbWFnZXM6IiwKICAgICAiIC0gdG90YWwgdHJhaW5pbmcgZG9nIGltYWdlczoge2xlbmd0aChsaXN0LmZpbGVzKHRyYWluX2RvZ3NfZGlyKSl9IiwKICAgICAiIC0gdG90YWwgdmFsaWRhdGlvbiBkb2cgaW1hZ2VzOiB7bGVuZ3RoKGxpc3QuZmlsZXModmFsaWRfZG9nc19kaXIpKX0iLAogICAgICIgLSB0b3RhbCB0ZXN0IGRvZyBpbWFnZXM6IHtsZW5ndGgobGlzdC5maWxlcyh0ZXN0X2RvZ3NfZGlyKSl9IiwKICAgICAuc2VwID0gIlxuIgogICAgICkKYGBgCgpMZXQncyBjaGVjayBvdXQgdGhlIGZpcnN0IDEwIGNhdCBhbmQgZG9nIGltYWdlczoKCmBgYHtyIGV4YW1wbGUtaW1hZ2VzfQpvcCA8LSBwYXIobWZyb3cgPSBjKDQsIDUpLCBwdHkgPSAicyIsIG1hciA9IGMoMC4xLCAwLjEsIDAuMSwgMC4xKSkKZm9yIChpIGluIDE6MTApIHsKICBwbG90KGFzLnJhc3RlcihqcGVnOjpyZWFkSlBFRyhwYXN0ZTAodHJhaW5fY2F0c19kaXIsICIvY2F0LiIsIGksICIuanBnIikpKSkKICBwbG90KGFzLnJhc3RlcihqcGVnOjpyZWFkSlBFRyhwYXN0ZTAodHJhaW5fZG9nc19kaXIsICIvZG9nLiIsIGksICIuanBnIikpKSkKfQpwYXIob3ApCmBgYAoKIyBDTk4gd2l0aCBpbWFnZSBnZW5lcmF0b3IKCiMjIERlZmluZSBhbmQgY29tcGlsZSBtb2RlbAoKV2UncmUgZ29pbmcgdG8gc2V0IHVwIGEgc2ltcGxlIENOTiBtb2RlbCB0aGF0IGNvbnRhaW5zIHN0ZXBzIHlvdSBzYXcgaW4gdGhlIApwcmV2aW91cyBtb2R1bGUuIFRoaXMgQ05OIGluY2x1ZGVzOgoKLSBGb3VyIHNlcXVlbnRpYWwgY29udiBhbmQgbWF4IHBvb2xpbmcgbGF5ZXJzCi0gRmxhdHRlbiBsYXllcgotIERlbnNseS1jb25uZWN0ZWQgbmV0d29yawotIFNpbmdsZSBiaW5hcnkgb3V0cHV0CgpgYGB7ciBjbm4tYXJjaGl0ZWN0dXJlfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgCiAgIyBmZWF0dXJlIGRldGVjdG9yIHBvcnRpb24gb2YgbW9kZWwKICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSAzMiwga2VybmVsX3NpemUgPSBjKDMsIDMpLCBhY3RpdmF0aW9uID0gInJlbHUiLCAKICAgICAgICAgICAgICAgIGlucHV0X3NoYXBlID0gYygxNTAsIDE1MCwgMykpICU+JQogIGxheWVyX21heF9wb29saW5nXzJkKHBvb2xfc2l6ZSA9IGMoMiwgMikpICU+JQogIAogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDY0LCBrZXJuZWxfc2l6ZSA9IGMoMywgMyksIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JSAKICBsYXllcl9tYXhfcG9vbGluZ18yZChwb29sX3NpemUgPSBjKDIsIDIpKSAlPiUKICAKICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSAxMjgsIGtlcm5lbF9zaXplID0gYygzLCAzKSwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lIAogIGxheWVyX21heF9wb29saW5nXzJkKHBvb2xfc2l6ZSA9IGMoMiwgMikpICU+JQogIAogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDEyOCwga2VybmVsX3NpemUgPSBjKDMsIDMpLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUgCiAgbGF5ZXJfbWF4X3Bvb2xpbmdfMmQocG9vbF9zaXplID0gYygyLCAyKSkgJT4lCiAgCiAgIyBjbGFzc2lmaWVyIHBvcnRpb24gb2YgbW9kZWwKICBsYXllcl9mbGF0dGVuKCkgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSA1MTIsIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSwgYWN0aXZhdGlvbiA9ICJzaWdtb2lkIikKCnN1bW1hcnkobW9kZWwpCmBgYAoKQ29tcGlsZSB0aGUgbW9kZWw6CgpgYGB7ciBjbm4tY29tcGlsZX0KbW9kZWwgJT4lIGNvbXBpbGUoCiAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwKICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfcm1zcHJvcChsciA9IDAuMDAwMSksCiAgbWV0cmljcyA9ICJhY2N1cmFjeSIKKQpgYGAKCiMjIFJlYWQgaW1hZ2VzIGZyb20gZGlyZWN0b3JpZXMKCk5leHQsIHdlIG5lZWQgYSBwcm9jZXNzIHRoYXQgaW1wb3J0cyBvdXIgaW1hZ2VzIGFuZCB0cmFuc2Zvcm1zIHRoZW0gdG8gdGVuc29ycwp0aGF0IG91ciBtb2RlbCBjYW4gcHJvY2Vzcy4gV2UnbGwgdXNlIHR3byBmdW5jdGlvbnMgdG8gcGVyZm9ybSB0aGlzIHByb2Nlc3MuCgpgaW1hZ2VfZGF0YV9nZW5lcmF0b3JgIHdpbGw6CgoxLiBSZWFkIHRoZSBpbWFnZSBmaWxlcwoyLiBEZWNvZGUgdGhlIGltYWdlIHRvIFJHQiBncmlkcyBvZiBwaXhlbHMKMy4gQ29udmVydCB0aGVzZSBpbnRvIGZsb2F0aW5nIHBvaW50IHRlbnNvcnMKNC4gUmVzY2FsZSBwaXhlbCB2YWx1ZXMgdG8gWzAsIDFdIGludGVydmFsCgpgaW1hZ2VfZGF0YV9nZW5lcmF0b3JgIHByb3ZpZGVzIG90aGVyIGNhcGFiaWxpdGllcyB0aGF0IHdlJ2xsIGxvb2sgYXQgc2hvcnRseS4KCmBmbG93X2ltYWdlc19mcm9tX2RpcmVjdG9yeWAgd2lsbDoKCjEuIEFwcGx5IGBpbWFnZV9kYXRhX2dlbmVyYXRvcmAKMi4gVG8gYSBiYXRjaCBvZiAyMCBpbWFnZXMgYXQgYSB0aW1lCjMuIEZyb20gb3VyIHRyYWluaW5nIGRpcmVjdG9yeSAocmFuZG9tbHkgc2h1ZmZsaW5nIGJldHdlZW4gc3ViZGlyZWN0b3JpZXMpCjQuIFJlc2l6ZSB0aGVzZSBpbWFnZXMgdG8gYmUgY29uc2lzdGVudCBzaXplIG9mIDE1MHgxNTAgcGl4ZWxzCjUuIEFwcGx5IGJpbmFyeSBsYWJlbHMKCmBgYHtyIGNubi1pbWFnZS1nZW5lcmF0b3J9CnRyYWluX2RhdGFnZW4gPC0gaW1hZ2VfZGF0YV9nZW5lcmF0b3IocmVzY2FsZSA9IDEvMjU1KQp2YWxpZF9kYXRhZ2VuIDwtIGltYWdlX2RhdGFfZ2VuZXJhdG9yKHJlc2NhbGUgPSAxLzI1NSkKCnRyYWluX2dlbmVyYXRvciA8LSBmbG93X2ltYWdlc19mcm9tX2RpcmVjdG9yeSgKICB0cmFpbl9kaXIsCiAgdHJhaW5fZGF0YWdlbiwKICB0YXJnZXRfc2l6ZSA9IGMoMTUwLCAxNTApLAogIGJhdGNoX3NpemUgPSAyMCwKICBjbGFzc19tb2RlID0gImJpbmFyeSIKKQoKdmFsaWRhdGlvbl9nZW5lcmF0b3IgPC0gZmxvd19pbWFnZXNfZnJvbV9kaXJlY3RvcnkoCiAgdmFsaWRfZGlyLAogIHZhbGlkX2RhdGFnZW4sCiAgdGFyZ2V0X3NpemUgPSBjKDE1MCwgMTUwKSwKICBiYXRjaF9zaXplID0gMjAsCiAgY2xhc3NfbW9kZSA9ICJiaW5hcnkiCikKYGBgCgpJZiB3ZSBnZXQgdGhlIGZpcnN0IGJhdGNoIGZyb20gdGhlIGdlbmVyYXRvciwgeW91IHdpbGwgc2VlIHRoYXQgaXQgeWllbGRzIDIwIAppbWFnZXMgb2YgMTUweDE1MCBwaXhlbHMgd2l0aCB0aHJlZSBjaGFubmVscyAoMjAsIDE1MCwgMTUwLCAzKSBhbG9uZyB3aXRoIHRoZWlyIApiaW5hcnkgbGFiZWxzICgwLCAxKS4KCmBgYHtyIGdlbmVyYXRvci1zdHJ1Y3R1cmV9CmJhdGNoIDwtIGdlbmVyYXRvcl9uZXh0KHRyYWluX2dlbmVyYXRvcikKc3RyKGJhdGNoKQpgYGAKCiMjIFRyYWluIHRoZSBtb2RlbAoKVG8gdHJhaW4gb3VyIG1vZGVsIHdlJ2xsIHVzZSBgZml0X2dlbmVyYXRvcmAgd2hpY2ggaXMgdGhlIGVxdWl2YWxlbnQgb2YgYGZpdGAgCmZvciBkYXRhIGdlbmVyYXRvcnMuICBXZSBwcm92aWRlIGl0IG91ciBnZW5lcmF0b3JzIGZvciB0aGUgdHJhaW5pbmcgYW5kIAp2YWxpZGF0aW9uIGRhdGEuICBQbHVzLCB3ZSBuZWVkIHRvIHNwZWNpZnk6CgotIGBzdGVwc19wZXJfZXBvY2hgOiBob3cgbWFueSBzYW1wbGVzIHRvIGRyYXcgZnJvbSB0aGUgdHJhaW5pbmcgZ2VuZXJhdG9yIGJlZm9yZSAKICBkZWNsYXJpbmcgYW4gZXBvY2ggb3Zlci4gT3VyIGdlbmVyYXRvciBzdXBwbGllcyBiYXRjaGVzIG9mIDIwIGFuZCB3ZSBoYXZlIAogIDIsMDAwIHRyYWluaW5nIGltYWdlcyBzbyB3ZSBuZWVkIDEwMCBzdGVwcy4KLSBgdmFsaWRhdGlvbl9zdGVwc2A6IGhvdyBtYW55IHNhbXBsZXMgdG8gZHJhdyBmcm9tIHRoZSB2YWxpZGF0aW9uIGdlbmVyYXRvci4gCiAgT3VyIGdlbmVyYXRvciBzdXBwbGllcyBiYXRjaGVzIG9mIDIwIGFuZCB3ZSBoYXZlIDEsMDAwIHZhbGlkYXRpb24gaW1hZ2VzIHNvIHdlCiAgbmVlZCA1MCBzdGVwcy4KICAKX19Ob3RlX186CgoqIFdpdGhvdXQgYSBHUFUgdGhpcyB3aWxsIHRha2UgYXBwcm94aW1hdGVseSAyMCBtaW51dGVzIHRvIHRyYWluCiogV2l0aCBHUFVzIHRoaXMgd2lsbCB0YWtlIGFwcHJveGltYXRlbHkgNSBtaW51dGVzCgpgYGB7ciBjbm4tdHJhaW59Cmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdF9nZW5lcmF0b3IoCiAgdHJhaW5fZ2VuZXJhdG9yLAogIHN0ZXBzX3Blcl9lcG9jaCA9IDEwMCwKICBlcG9jaHMgPSAzMCwKICB2YWxpZGF0aW9uX2RhdGEgPSB2YWxpZGF0aW9uX2dlbmVyYXRvciwKICB2YWxpZGF0aW9uX3N0ZXBzID0gNTAsCiAgY2FsbGJhY2tzID0gY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSA1KQopCmBgYAoKT3VyIGZpcnN0IG1vZGVsJ3MgcGVyZm9ybWFuY2UgaXMgbm90IHRoYXQgYmFkIGJ1dCBkZWZpbml0ZWx5IGhhcyByb29tIGZvcgppbXByb3ZlbWVudC4KCmBgYHtyIGluaXRpYWwtbW9kZWwtcmVzdWx0c30KYmVzdF9lcG9jaCA8LSB3aGljaC5taW4oaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzKQpiZXN0X2xvc3MgPC0gaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzW2Jlc3RfZXBvY2hdICU+JSByb3VuZCgzKQpiZXN0X2FjYyA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX2FjY3VyYWN5W2Jlc3RfZXBvY2hdICU+JSByb3VuZCgzKQoKZ2x1ZSgiT3VyIG9wdGltYWwgbG9zcyBpcyB7YmVzdF9sb3NzfSB3aXRoIGFuIGFjY3VyYWN5IG9mIHtiZXN0X2FjY30iKQpgYGAKCgpgYGB7ciBwbG90LWhpc3Rvcnl9CnBsb3QoaGlzdG9yeSkgKyAKICBzY2FsZV94X2NvbnRpbnVvdXMobGltaXRzID0gYygwLCBsZW5ndGgoaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzKSkpCmBgYAoKCiMgQ05OIHdpdGggSW1hZ2UgQXVnbWVudGF0aW9uCgojIyBJbWFnZSBBdWdtZW50YXRpb24KCk91ciBtb2RlbCBhYm92ZSBkb2VzIG9rIGJ1dCBkZWZpbml0ZWx5IGhhcyByb29tIGZvciBpbXByb3ZlbWVudC4gT25lIGFwcHJvYWNoIAp0byBpbXByb3ZlIHBlcmZvcm1hbmNlIGlzIHRvIGNvbGxlY3QgbW9yZSBkYXRhLiBVbmZvcnR1bmF0ZWx5LCB0aGlzIGlzIG5vdCBhbHdheXMgCmFuIG9wdGlvbi4gQW4gYWx0ZXJuYXRpdmUgaXMgdG8gdXNlIF9fX2ltYWdlIGF1Z21lbnRhdGlvbl9fXy4gW+KEue+4j10oaHR0cDovL2JpdC5seS9kbC0wMyMzOCkKCmBgYHtyIGltYWdlLWF1Z21lbnRhdGlvbn0KZGF0YWdlbiA8LSBpbWFnZV9kYXRhX2dlbmVyYXRvcigKICByZXNjYWxlID0gMS8yNTUsCiAgcm90YXRpb25fcmFuZ2UgPSA0MCwKICB3aWR0aF9zaGlmdF9yYW5nZSA9IDAuMiwKICBoZWlnaHRfc2hpZnRfcmFuZ2UgPSAwLjIsCiAgc2hlYXJfcmFuZ2UgPSAwLjIsCiAgem9vbV9yYW5nZSA9IDAuMiwKICBob3Jpem9udGFsX2ZsaXAgPSBUUlVFLAogIGZpbGxfbW9kZSA9ICJuZWFyZXN0IgopCmBgYAoKVGhlIGZvbGxvd2luZyBoZWxwcyB0byB2aXN1YWxpemUgdGhlIGlkZWEgb2YgaW1hZ2UgYXVnbWVudGF0aW9uIGJ5OgoKLSBSZWFkaW5nIGluIHRoZSBmaXJzdCBpbWFnZSBhbmQgcmVzaXppbmcgaXQgdG8gMTUweDE1MCwKLSBDb252ZXJ0aW5nIGl0IHRvIGFuIGFycmF5IHdpdGggc2hhcGUgKDE1MCwgMTUwLCAzKSwKLSBSZXNoYXBpbmcgaXQgdG8gKDEsIDE1MCwgMTUwLCAzKSwKLSBHZW5lcmF0aW5nIGJhdGNoZXMgb2YgcmFuZG9tbHkgdHJhbnNmb3JtZWQgaW1hZ2VzLgoKYGBge3Igdmlldy1hdWdtZW50ZWQtaW1hZ2VzfQojIGdldCB0aGUgZmlyc3QgY2F0IGltYWdlCmZuYW1lcyA8LSBsaXN0LmZpbGVzKHRyYWluX2NhdHNfZGlyLCBmdWxsLm5hbWVzID0gVFJVRSkKaW1nX3BhdGggPC0gZm5hbWVzW1sxXV0KCiMgcmVzaXplICYgcmVzaGFwZQppbWcgPC0gaW1hZ2VfbG9hZChpbWdfcGF0aCwgdGFyZ2V0X3NpemUgPSBjKDE1MCwgMTUwKSkKaW1nX2FycmF5IDwtIGltYWdlX3RvX2FycmF5KGltZykKaW1nX2FycmF5IDwtIGFycmF5X3Jlc2hhcGUoaW1nX2FycmF5LCBjKDEsIDE1MCwgMTUwLCAzKSkKCiMgZ2VuZXJhdGUgYSBhIHNpbmdsZSBhdWdtZW50ZWQgaW1hZ2UKYXVnbWVudGF0aW9uX2dlbmVyYXRvciA8LSBmbG93X2ltYWdlc19mcm9tX2RhdGEoCiAgaW1nX2FycmF5LAogIGdlbmVyYXRvciA9IGRhdGFnZW4sCiAgYmF0Y2hfc2l6ZSA9IDEKKQoKIyBwbG90IDEwIGF1Z21lbnRlZCBpbWFnZXMgb2YgdGhlIGZpcnN0IGNhdCBpbWFnZQpvcCA8LSBwYXIobWZyb3cgPSBjKDIsIDUpLCBwdHkgPSAicyIsIG1hciA9IGMoMCwgMC4xLCAwLCAwLjEpKQpmb3IgKGkgaW4gMToxMCkgewogIGJhdGNoIDwtIGdlbmVyYXRvcl9uZXh0KGF1Z21lbnRhdGlvbl9nZW5lcmF0b3IpCiAgcGxvdChhcy5yYXN0ZXIoYmF0Y2hbMSwsLF0pKQp9CnBhcihvcCkKYGBgCgojIyBCdWlsZCAmIHRyYWluIG1vZGVsCgpMZXQncyBjcmVhdGUgYSBuZXcgbW9kZWwgdGhhdCBpbmNsdWRlcyBpbWFnZSBhdWdtZW50YXRpb24gYW5kIHdlJ2xsIGFwcGx5IHRoZSAKZHJvcG91dCByZWd1bGFyaXphdGlvbiBtZXRob2QuIFRoZSBmb2xsb3dpbmcgY3JlYXRlcyBhIENOTiBhcmNoaXRlY3R1cmUgd2l0aDoKCi0gRm91ciBzZXF1ZW50aWFsIGNvbnYgYW5kIG1heCBwb29saW5nIGxheWVycwotIEZsYXR0ZW4gbGF5ZXIKLSBEcm9wb3V0IGxheWVyCi0gRGVuc2x5LWNvbm5lY3RlZCBuZXR3b3JrCgpBbGwgb2Ygd2hpY2ggeW91IGFyZSBmYW1pbGlhcnkgd2l0aCBieSBub3cuCgpgYGB7ciBjbm4tc3RydWN0dXJlMn0KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDMyLCBrZXJuZWxfc2l6ZSA9IGMoMywgMyksIGFjdGl2YXRpb24gPSAicmVsdSIsIAogICAgICAgICAgICAgICAgaW5wdXRfc2hhcGUgPSBjKDE1MCwgMTUwLCAzKSkgJT4lCiAgbGF5ZXJfbWF4X3Bvb2xpbmdfMmQocG9vbF9zaXplID0gYygyLCAyKSkgJT4lCiAgbGF5ZXJfY29udl8yZChmaWx0ZXJzID0gNjQsIGtlcm5lbF9zaXplID0gYygzLCAzKSwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lIAogIGxheWVyX21heF9wb29saW5nXzJkKHBvb2xfc2l6ZSA9IGMoMiwgMikpICU+JQogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDEyOCwga2VybmVsX3NpemUgPSBjKDMsIDMpLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUgCiAgbGF5ZXJfbWF4X3Bvb2xpbmdfMmQocG9vbF9zaXplID0gYygyLCAyKSkgJT4lCiAgbGF5ZXJfY29udl8yZChmaWx0ZXJzID0gMTI4LCBrZXJuZWxfc2l6ZSA9IGMoMywgMyksIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JSAKICBsYXllcl9tYXhfcG9vbGluZ18yZChwb29sX3NpemUgPSBjKDIsIDIpKSAlPiUKICBsYXllcl9mbGF0dGVuKCkgJT4lCiAgbGF5ZXJfZHJvcG91dChyYXRlID0gMC41KSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDUxMiwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxLCBhY3RpdmF0aW9uID0gInNpZ21vaWQiKQoKbW9kZWwgJT4lIGNvbXBpbGUoCiAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwKICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfcm1zcHJvcChsciA9IDAuMDAwMSksCiAgbWV0cmljcyA9ICJhY2N1cmFjeSIKKQpgYGAKCk5vdyB3ZSBjYW4gYWRkIGltYWdlIGF1Z21lbnRhdGlvbiB0byBvdXIgYGltYWdlX2RhdGFfZ2VuZXJhdG9yKClgLiBUaGUgcmVzdCBvZiAKdGhlIGlucHV0cyByZW1haW4gdGhlIHNhbWUuCgpfX05vdGVfXzoKCiogV2l0aG91dCBhIEdQVSB0aGlzIHdpbGwgdGFrZSBhcHByb3hpbWF0ZWx5IDYwIG1pbnV0ZXMgdG8gdHJhaW4KKiBXaXRoIEdQVXMgdGhpcyB3aWxsIHRha2UgYXBwcm94aW1hdGVseSAyMC0zMCBtaW51dGVzCgpgYGB7ciBhdWdtZW50LWFuZC10cmFpbn0KIyBvbmx5IGF1Z21lbnQgdHJhaW5pbmcgZGF0YQp0cmFpbl9kYXRhZ2VuIDwtIGltYWdlX2RhdGFfZ2VuZXJhdG9yKAogIHJlc2NhbGUgPSAxLzI1NSwKICByb3RhdGlvbl9yYW5nZSA9IDQwLAogIHdpZHRoX3NoaWZ0X3JhbmdlID0gMC4yLAogIGhlaWdodF9zaGlmdF9yYW5nZSA9IDAuMiwKICBzaGVhcl9yYW5nZSA9IDAuMiwKICB6b29tX3JhbmdlID0gMC4yLAogIGhvcml6b250YWxfZmxpcCA9IFRSVUUsCikKCiMgZG8gbm90IGF1Z21lbnQgdGVzdCBhbmQgdmFsaWRhdGlvbiBkYXRhCnRlc3RfZGF0YWdlbiA8LSBpbWFnZV9kYXRhX2dlbmVyYXRvcihyZXNjYWxlID0gMS8yNTUpCgojIGdlbmVyYXRlIGJhdGNoZXMgb2YgZGF0YSBmcm9tIHRyYWluaW5nIGRpcmVjdG9yeQp0cmFpbl9nZW5lcmF0b3IgPC0gZmxvd19pbWFnZXNfZnJvbV9kaXJlY3RvcnkoCiAgdHJhaW5fZGlyLAogIHRyYWluX2RhdGFnZW4sCiAgdGFyZ2V0X3NpemUgPSBjKDE1MCwgMTUwKSwKICBiYXRjaF9zaXplID0gMjAsCiAgY2xhc3NfbW9kZSA9ICJiaW5hcnkiCikKCiMgZ2VuZXJhdGUgYmF0Y2hlcyBvZiBkYXRhIGZyb20gdmFsaWRhdGlvbiBkaXJlY3RvcnkKdmFsaWRhdGlvbl9nZW5lcmF0b3IgPC0gZmxvd19pbWFnZXNfZnJvbV9kaXJlY3RvcnkoCiAgdmFsaWRfZGlyLAogIHRlc3RfZGF0YWdlbiwKICB0YXJnZXRfc2l6ZSA9IGMoMTUwLCAxNTApLAogIGJhdGNoX3NpemUgPSAyMCwKICBjbGFzc19tb2RlID0gImJpbmFyeSIKKQoKIyB0cmFpbiBtb2RlbApoaXN0b3J5IDwtIG1vZGVsICU+JQogIGZpdF9nZW5lcmF0b3IoCiAgICB0cmFpbl9nZW5lcmF0b3IsCiAgICBzdGVwc19wZXJfZXBvY2ggPSAxMDAsCiAgICBlcG9jaHMgPSAxMDAsCiAgICB2YWxpZGF0aW9uX2RhdGEgPSB2YWxpZGF0aW9uX2dlbmVyYXRvciwKICAgIHZhbGlkYXRpb25fc3RlcHMgPSA1MCwKICAgIGNhbGxiYWNrcyA9IGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMTApCiAgKQpgYGAKCkFzIHlvdSBjYW4gc2VlLCB1c2luZyBpbWFnZSBhdWdtZW50YXRpb24gaGVscHMgdG8gaW1wcm92ZSBvdXIgbW9kZWwncwpwZXJmb3JtYW5jZS4gSW4gZmFjdCwgaWYgd2UgaGFkIG1vcmUgYHBhdGllbmNlYCB3aXRoIG91ciBlYXJseSBzdG9wcGluZyB3ZSBtYXkKZXZlbiBiZSBhYmxlIHRvIG51ZGdlIG91dCBhIGxpdHRsZSBtb3JlIGxvc3MgcmVkdWN0aW9uLgoKYGBge3Igc2Vjb25kLW1vZGVsLXJlc3VsdHN9CmJlc3RfZXBvY2ggPC0gd2hpY2gubWluKGhpc3RvcnkkbWV0cmljcyR2YWxfbG9zcykKYmVzdF9sb3NzIDwtIGhpc3RvcnkkbWV0cmljcyR2YWxfbG9zc1tiZXN0X2Vwb2NoXSAlPiUgcm91bmQoMykKYmVzdF9hY2MgPC0gaGlzdG9yeSRtZXRyaWNzJHZhbF9hY2N1cmFjeVtiZXN0X2Vwb2NoXSAlPiUgcm91bmQoMykKCmdsdWUoIk91ciBvcHRpbWFsIGxvc3MgaXMge2Jlc3RfbG9zc30gd2l0aCBhbiBhY2N1cmFjeSBvZiB7YmVzdF9hY2N9IikKYGBgCgoKYGBge3Igc2Vjb25kLW1vZGVsLXBsb3R9CnBsb3QoaGlzdG9yeSkgKyAKICBzY2FsZV94X2NvbnRpbnVvdXMobGltaXRzID0gYygwLCBsZW5ndGgoaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzKSkpCmBgYAoKIyMgU2F2ZSB0aGUgbW9kZWwKCldlIGNhbiBhbHdheXMgc2F2ZSBvdXIgbW9kZWxzIGFzIGg1IGZpbGVzLiBMZXQncyBzYXZlIHRoaXMgbW9kZWwgYXMgd2Ugd2lsbCB1c2UKaXQgb25lIG9mIHRoZSAiZXh0cmFzIiBub3RlYm9va3MgdG8gaWxsdXN0cmF0ZSBob3cgd2UgY2FuIHZpc3VhbGl6ZSBDTk5zIChzZWUKdGhpcyBub3RlYm9vayBodHRwczovL3JzdHVkaW8tY29uZi0yMDIwLmdpdGh1Yi5pby9kbC1rZXJhcy10Zi9ub3RlYm9va3MvdmlzdWFsaXppbmctd2hhdC1jbm5zLWxlYXJuLm5iLmh0bWwpLgoKYGBge3Igc2F2ZS1tb2RlbH0KbW9kZWwgJT4lIHNhdmVfbW9kZWxfaGRmNSgiY2F0c19hbmRfZG9nc19zbWFsbF8xLmg1IikKYGBgCgpIb3dldmVyLCB3ZSBzdGlsbCBoYXZlIHJvb20gZm9yIGltcHJvdmVtZW50IGJlY2F1c2Ugd2UgYXJlIG9ubHkgdXNpbmcgYSBzbWFsbApzdWJzZXQgb2YgdGhlIGF2YWlsYWJsZSBkYXRhLiBXZSBoYXZlIHR3byBvcHRpb25zIHRvIGltcHJvdmUgb3VyIG1vZGVsOgoKMS4gVXNlIG1vcmUgZGF0YS4gV2UgYXJlIG9ubHkgdXNpbmcgMiwwMDAgb2YgdGhlIDI1LDAwMCBhdmFpbGFibGUgaW1hZ2VzLgogICBIb3dldmVyLCB0aGlzIHdvdWxkIGhhdmUgYSBzaWduaWZpY2FudCBpbXBhY3Qgb24gY29tcHV0ZSB0aW1lLgogICAKMi4gVXNlIHRyYW5zZmVyIGxlYXJuaW5nLiBUaGlzIGlzIG11Y2ggcXVpY2tlciB0aGFuIHRoZSBmaXJzdCBvcHRpb24gc28gaW4gdGhlCiAgIG5leHQgbW9kdWxlIEkgZGVtb25zdHJhdGUgaG93IHRvIHVzZSB0cmFuc2ZlciBsZWFybmluZyBmb3IgQ05Ocy4KCiMgVGFrZXdheXMKCiogV2hlbiB1c2luZyBpbWFnZSBkYXRhIHdlLi4uCiAgIC0gdXNlIGBpbWFnZV9kYXRhX2dlbmVyYXRvcmAgdG8gcmVhZCB0aGUgaW1hZ2VzLCBkZWNvZGUgcGl4ZWwgdmFsdWVzLCBjb252ZXJ0CiAgICAgdG8gYSB0ZW5zb3IsIHJlc2NhbGUsIGFuZCBwZXJmb3JtIGltYWdlIGF1Z21lbnRhdGlvbi4KICAgLSB1c2UgYGZsb3dfaW1hZ2VzX2Zyb21fZGlyZWN0b3J5YCBpbXBvcnQgYmF0Y2hlcyBvZiBvdXIgaW1hZ2VzLCBhcHBseSB0aGUKICAgICBgaW1hZ2VfZGF0YV9nZW5lcmF0b3JgLCByZXNpemUsIGFuZCBpbmZlciB0cmFpbmluZyBsYWJlbHMuCiAgICAgCiogSW1hZ2UgYXVnbWVudGF0aW9uIHN1Y2ggYXMgem9vbWluZywgZmxpcHBpbmcsIHJvdGF0aW5nLCBzaGVhcmluZywgZXRjLiBoZWxwcwogIHdpdGggaW1hZ2UgdmFyaWFuY2UsIHByb3ZpZGVzIGZyZWUgYWRkaXRpb25hbCBkYXRhLCBhbmQgZ2VuZXJhbGx5IGltcHJvdmVzCiAgbW9kZWwgcGVyZm9ybWFuY2UuCiAgClvwn4+gXShodHRwczovL2dpdGh1Yi5jb20vcnN0dWRpby1jb25mLTIwMjAvZGwta2VyYXMtdGYp