In this module, we are going to use a pretrained CNN model to perform image classification on our dogs vs. cats images.

Learning objectives:

  • Why using pretrained models can be efficient and effective.
  • How to perform feature extraction with pretrained models.
  • How you can fine tune a pretrained model and run end-to-end.

Requirements

library(keras)

Data

We are working with the same dogs and cats images as before.

# 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")

Transfer Learning

There are two main ways we can apply a pretrained model to perform a CNN

  1. Feature extraction: Use the convolutional base to do feature engineering on our images and then feed into a new densely connected classifier.
    • Most efficient
    • Does not require GPUs
    • Does not “personalize” feature extraction to the problem at hand
    • Likely leaves room for improvement
  2. Fine tune a pretrained model and run end-to-end: Build a full sequential model with the convolutional base and a new densely connected classifier and train the entire model with some or all of the convolutional base layers frozen.
    • Computationally demanding
    • Often requires GPUs
    • Tweaks pretrained convolution layers to extract problem-specific features
    • Maximize performance

Transfer Learning: Feature Extraction

Get our pretrained model

There are several pretrained models available via keras; they all start with application_. Here we’ll use the VGG16 model; it is intuitive to understand the model structure, does a good job with this task, and is good for teaching purposes.

Best practice for picking pretrained models

  • Start with smaller models (i.e. VGG16, VGG19, Resnet34)
  • Check out the data the model was built on and see how it aligns to your data. Most common is ImageNet

Applying pretrained models that are already supplied by keras is simple:

  • weights: Represents the weights to use. Most pretrained models are built on imagenet and using these weights tends to do well.
  • include_top: Whether to include the fully-connected dense classifier. Typically, we want the classifier to be specific to our problem.
  • input_shape: The shape of our nputs (150x150 pixel images w/3 color channels).

Tip: Check out the tfhub package which makes it easy to interact with TensorFlow Hub, a libarry for publication, discovery, and consumption of resusable models.

conv_base <- application_vgg16(
  weights = "imagenet",
  include_top = FALSE,
  input_shape = c(150, 150, 3)
)
summary(conv_base)

Extracting features using the pretrained convolutional base

This seems a little daunting to understand but this is just implementing more of a manual approach to what you already have been doing. Here we create a function that will:

  1. Create an empty tensor to hold our transformed features and labels.
  2. Create our data batch importing generator. Here we will use batch size of 20 simply because we have 2000 training images and 1000 validation images and a batch size of 20 divides nicely to both of these values.
  3. Loop through our training data:
    1. import a batch of images
    2. apply the pretrained base to our images to output the predicted features
    3. add these new features and the labels to our tensor
    4. continue to do this until our number of iterations x batch size equals or exceeds our total number of samples.

datagen <- image_data_generator(rescale = 1/255)
batch_size <- 20

extract_features <- function(directory, sample_count) {
  
  features <- array(0, dim = c(sample_count, 4, 4, 512))   # step 1
  labels <- array(0, dim = c(sample_count))                # step 1
  
  generator <- flow_images_from_directory(                 # step 2
    directory = directory,                                 
    generator = datagen,                                   
    target_size = c(150, 150),
    batch_size = batch_size,
    class_mode = "binary"
  )
  
  i <- 0
  while (TRUE) {                                           # step 3
    message("Processing batch ", i + 1, " of ", ceiling(sample_count / batch_size))
    batch <- generator_next(generator)                     # step 3a
    inputs_batch <- batch[[1]]
    labels_batch <- batch[[2]]
    features_batch <- conv_base %>% predict(inputs_batch)  # step 3b
    index_range <- ((i * batch_size) + 1):((i + 1) * batch_size)
    features[index_range,,,] <- features_batch             # step 3c
    labels[index_range] <- labels_batch                    # step 3c
    i <- i + 1
    if (i * batch_size >= sample_count) break              # step 3d
  }
  
  list(
    features = features,
    labels = labels
  ) 
}

Let’s apply this function to our training, validation, and test data

Without a GPU this will take approximately 5 minutes to execute

train <- extract_features(train_dir, 2000)
validation <- extract_features(valid_dir, 1000)
test <- extract_features(test_dir, 1000)

Reshape features

The extracted features will be a 4D tensor (samples, 4, 4, 512). We can see this in the last layer of our conv_base model above (block5_pool (MaxPooling2D)).

Consequently, we need to reshape (flatten) these into a 2D tensor to feed into a densely connected classifier. This results in a 2D tensor of size (samples, 4 * 4 * 512 = 8192).

reshape_features <- function(features) {
  array_reshape(features, dim = c(nrow(features), 4 * 4 * 512))
}

train$features <- reshape_features(train$features)
validation$features <- reshape_features(validation$features)
test$features <- reshape_features(test$features)

dim(train$features)

Define densely connected classifier

We’ve extracted and flattened our features from the convolution layers so now we only need to build the densely connected classifier portion of our model.

model <- keras_model_sequential() %>%
  layer_dense(units = 256, activation = "relu", input_shape = 4 * 4 * 512) %>%
  layer_dropout(rate = 0.5) %>%
  layer_dense(units = 1, activation = "sigmoid")

summary(model)

Now we can compile and train our model. This will train quickly, taking approximately 1 min when trained on your local CPU. Our validation loss also improves over the previous CNN we built.

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

history1 <- model %>% fit(
  train$features, train$labels,
  epochs = 30,
  batch_size = 32,
  validation_data = list(validation$features, validation$labels),
  callbacks = list(
    callback_early_stopping(patience = 10),
    callback_reduce_lr_on_plateau(patience = 2)
  )
)

So we acheived a significant decrease in our loss score, increased our accuracy to 90%, and did so in a fraction of the time!

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

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

Transfer Learning: End-to-End

⚠️⚠️ ONLY RUN ON GPU!! ⚠️⚠️

The above approach performed pretty well. However, we can see that we are still overfitting, which may be reducing model performance. An alternative approach is to run a pretrained model from end-to-end. This approach is much slower and computationally intense; however, it offers greater flexibility in using and adjusting the pretrained model because it lets you:

  1. use data augmentation to decrease overfitting (and usually increase model performance).
  2. fine tune parts of the pretrained model.

The following approach simply plugs the pretrained convolution base into a sequential model but freezes the convolution base weights.

Combining a densely-connected neural network with the convolutional base

In this case we can literally plug in our conv_base within our model architecture.


model <- keras_model_sequential() %>%
  conv_base %>%
  layer_flatten() %>%
  layer_dense(units = 256, activation = "relu") %>%
  layer_dense(units = 1, activation = "sigmoid")

model

Freezing layers

Before you compile and train the model, it’s important to freeze the convolutional base weights. This prevents the weights from being updated during training. If you don’t do this then the representations found in the pretrained model will be modified and, potentially, completely destroyed.

cat(length(model$trainable_weights), "trainable weight tensors before freezing.\n")

freeze_weights(conv_base)

cat(length(model$trainable_weights), "trainable weight tensors before freezing.\n")

Training the model end-to-end with a frozen convolutional base

The following trains the model end-to-end using all CNN logic that you have seen before:

  1. data augmentation
  2. image generator
  3. compile our model
  4. train our model

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,
  fill_mode = "nearest"
)

test_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(
  validation_dir,
  test_datagen,
  target_size = c(150, 150),
  batch_size = 20,
  class_mode = "binary"
)

model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 1e-5),
  metrics = c("accuracy")
)

history2 <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 100,
  epochs = 30,
  validation_data = validation_generator,
  validation_steps = 50
)
plot(history2)

Transfer Learning: Fine Tune

Another widely used technique for using pretrained models, is to unfreeze a few of the convolutional base and allow those weights to be updated. Recall that the early layers in a CNN identify detailed edges and shapes. Later layers put these edges and shapes together to make higher order parts of the images we are trying to classify (i.e. cat ears, dog tails).

The more our images deviate from the images used to create the pretrained model, then the more likely you will want to retrain the last few layers, which will make the edge and shape features more relevant to your problem.

To fine-tune a pretrained model you:

  1. Add your custom network on top of an already-trained base network (executed in the CNN-base-and-classifier code chunk).
  2. Freeze the base network (executed in the freeze-parameters code chunk).
  3. Train the part you added (executed in the train-end-to-end code chunk).
  4. Unfreeze some layers in the base network.
  5. Jointly train both these layers and the part you added.

We already did steps 1-3. The following executes steps 4 and 5.

Unfreeze some layers in the base network

unfreeze_weights(conv_base, from = "block3_conv1")

Jointly train both these layers and the part you added

model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 1e-5),
  metrics = c("accuracy")
)

history2 <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 100,
  epochs = 100,
  validation_data = validation_generator,
  validation_steps = 50
)
plot(history2)

Takeways

  • Pre-trained models can be efficient and effective for problems that align to common computer vision (and other) tasks.

  • Many pre-existing models exist on TensorFlow Hub and are worth researching.

  • Three main approaches exists for transfer learning; the decision depends on your desire to maximize efficiency (compute time) vs effectiveness (accuracy):
    • feature extraction
    • end-to-end training with existing CNN weights
    • end-toend training while fine-tuning latter CNN layer weights

🏠

LS0tCnRpdGxlOiAiQ29tcHV0ZXIgdmlzaW9uICYgQ05OczogVHJhbnNmZXIgTGVhcm5pbmciCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDogdHJ1ZQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIG1lc3NhZ2UgPSBGQUxTRSwgd2FybmluZyA9IEZBTFNFKQpnZ3Bsb3QyOjp0aGVtZV9zZXQoZ2dwbG90Mjo6dGhlbWVfYncoKSkKYGBgCgpJbiB0aGlzIG1vZHVsZSwgd2UgYXJlIGdvaW5nIHRvIHVzZSBhIF9fX3ByZXRyYWluZWRfX18gQ05OIG1vZGVsIHRvIHBlcmZvcm0KaW1hZ2UgY2xhc3NpZmljYXRpb24gb24gb3VyIGRvZ3MgdnMuIGNhdHMgaW1hZ2VzLiAKCkxlYXJuaW5nIG9iamVjdGl2ZXM6CgotIFdoeSB1c2luZyBwcmV0cmFpbmVkIG1vZGVscyBjYW4gYmUgZWZmaWNpZW50IGFuZCBlZmZlY3RpdmUuCi0gSG93IHRvIHBlcmZvcm0gZmVhdHVyZSBleHRyYWN0aW9uIHdpdGggcHJldHJhaW5lZCBtb2RlbHMuCi0gSG93IHlvdSBjYW4gZmluZSB0dW5lIGEgcHJldHJhaW5lZCBtb2RlbCBhbmQgcnVuIGVuZC10by1lbmQuCgojIFJlcXVpcmVtZW50cwoKYGBge3J9CmxpYnJhcnkoa2VyYXMpCmxpYnJhcnkoZ2x1ZSkKbGlicmFyeShnZ3Bsb3QyKQpgYGAKCiMgRGF0YQoKV2UgYXJlIHdvcmtpbmcgd2l0aCB0aGUgc2FtZSBkb2dzIGFuZCBjYXRzIGltYWdlcyBhcyBiZWZvcmUuCgpgYGB7ciBpbWFnZS1maWxlLXBhdGhzfQojIGRlZmluZSB0aGUgZGlyZWN0b3JpZXM6CmlmIChzdHJpbmdyOjpzdHJfZGV0ZWN0KGhlcmU6OmhlcmUoKSwgImNvbmYtMjAyMC11c2VyIikpIHsKICBpbWFnZV9kaXIgPC0gIi9ob21lL2NvbmYtMjAyMC11c2VyL2RhdGEvZG9ncy12cy1jYXRzIgp9IGVsc2UgewogIGltYWdlX2RpciA8LSBoZXJlOjpoZXJlKCJtYXRlcmlhbHMiLCAiZGF0YSIsICJkb2dzLXZzLWNhdHMiKQp9CnRyYWluX2RpciA8LSBmaWxlLnBhdGgoaW1hZ2VfZGlyLCAidHJhaW4iKQp2YWxpZF9kaXIgPC0gZmlsZS5wYXRoKGltYWdlX2RpciwgInZhbGlkYXRpb24iKQp0ZXN0X2RpciA8LSBmaWxlLnBhdGgoaW1hZ2VfZGlyLCAidGVzdCIpCmBgYAoKIyBUcmFuc2ZlciBMZWFybmluZwoKVGhlcmUgYXJlIF90d28gbWFpbl8gd2F5cyB3ZSBjYW4gYXBwbHkgYSBwcmV0cmFpbmVkIG1vZGVsIHRvIHBlcmZvcm0gYSBDTk4KCjEuIF9fRmVhdHVyZSBleHRyYWN0aW9uX186IFVzZSB0aGUgY29udm9sdXRpb25hbCBiYXNlIHRvIGRvIGZlYXR1cmUgZW5naW5lZXJpbmcKICAgb24gb3VyIGltYWdlcyBhbmQgdGhlbiBmZWVkIGludG8gYSBuZXcgZGVuc2VseSBjb25uZWN0ZWQgY2xhc3NpZmllci4KICAgLSBNb3N0IGVmZmljaWVudAogICAtIERvZXMgbm90IHJlcXVpcmUgR1BVcwogICAtIERvZXMgbm90ICJwZXJzb25hbGl6ZSIgZmVhdHVyZSBleHRyYWN0aW9uIHRvIHRoZSBwcm9ibGVtIGF0IGhhbmQKICAgLSBMaWtlbHkgbGVhdmVzIHJvb20gZm9yIGltcHJvdmVtZW50CiAgIAoyLiBfX0ZpbmUgdHVuZSBhIHByZXRyYWluZWQgbW9kZWwgYW5kIHJ1biBlbmQtdG8tZW5kX186IEJ1aWxkIGEgZnVsbCBzZXF1ZW50aWFsCiAgIG1vZGVsIHdpdGggdGhlIGNvbnZvbHV0aW9uYWwgYmFzZSBhbmQgYSBuZXcgZGVuc2VseSBjb25uZWN0ZWQgY2xhc3NpZmllciBhbmQKICAgdHJhaW4gdGhlIGVudGlyZSBtb2RlbCB3aXRoIHNvbWUgb3IgYWxsIG9mIHRoZSBjb252b2x1dGlvbmFsIGJhc2UgbGF5ZXJzCiAgIGZyb3plbi4KICAgLSBDb21wdXRhdGlvbmFsbHkgZGVtYW5kaW5nCiAgIC0gT2Z0ZW4gcmVxdWlyZXMgR1BVcwogICAtIFR3ZWFrcyBwcmV0cmFpbmVkIGNvbnZvbHV0aW9uIGxheWVycyB0byBleHRyYWN0IHByb2JsZW0tc3BlY2lmaWMgZmVhdHVyZXMKICAgLSBNYXhpbWl6ZSBwZXJmb3JtYW5jZQogICAKIVtdKGltYWdlcy9zd2FwcGluZ19mY19jbGFzc2lmaWVyLnBuZykKCiMgIFRyYW5zZmVyIExlYXJuaW5nOiBGZWF0dXJlIEV4dHJhY3Rpb24KCiMjIEdldCBvdXIgcHJldHJhaW5lZCBtb2RlbAoKVGhlcmUgYXJlIHNldmVyYWwgcHJldHJhaW5lZCBtb2RlbHMgYXZhaWxhYmxlIHZpYSBrZXJhczsgdGhleSBhbGwgc3RhcnQgd2l0aCAKYGFwcGxpY2F0aW9uX2AuIEhlcmUgd2UnbGwgdXNlIHRoZSBWR0cxNiBtb2RlbDsgaXQgaXMgaW50dWl0aXZlIHRvIHVuZGVyc3RhbmQgCnRoZSBtb2RlbCBzdHJ1Y3R1cmUsIGRvZXMgYSBnb29kIGpvYiB3aXRoIHRoaXMgdGFzaywgYW5kIGlzIGdvb2QgZm9yIHRlYWNoaW5nCnB1cnBvc2VzLgoKQmVzdCBwcmFjdGljZSBmb3IgcGlja2luZyBwcmV0cmFpbmVkIG1vZGVscwoKLSBTdGFydCB3aXRoIHNtYWxsZXIgbW9kZWxzIChpLmUuIFZHRzE2LCBWR0cxOSwgUmVzbmV0MzQpCi0gQ2hlY2sgb3V0IHRoZSBkYXRhIHRoZSBtb2RlbCB3YXMgYnVpbHQgb24gYW5kIHNlZSBob3cgaXQgYWxpZ25zIHRvIHlvdXIgZGF0YS4KICBNb3N0IGNvbW1vbiBpcyBbSW1hZ2VOZXRdKGh0dHA6Ly9pbWFnZS1uZXQub3JnL2Fib3V0LW92ZXJ2aWV3KQoKQXBwbHlpbmcgcHJldHJhaW5lZCBtb2RlbHMgdGhhdCBhcmUgYWxyZWFkeSBzdXBwbGllZCBieSBrZXJhcyBpcyBzaW1wbGU6CgotIGB3ZWlnaHRzYDogUmVwcmVzZW50cyB0aGUgd2VpZ2h0cyB0byB1c2UuIE1vc3QgcHJldHJhaW5lZCBtb2RlbHMgYXJlIGJ1aWx0IG9uIAogIGltYWdlbmV0IGFuZCB1c2luZyB0aGVzZSB3ZWlnaHRzIHRlbmRzIHRvIGRvIHdlbGwuCi0gYGluY2x1ZGVfdG9wYDogV2hldGhlciB0byBpbmNsdWRlIHRoZSBmdWxseS1jb25uZWN0ZWQgZGVuc2UgY2xhc3NpZmllci4gVHlwaWNhbGx5LCAKICB3ZSB3YW50IHRoZSBjbGFzc2lmaWVyIHRvIGJlIHNwZWNpZmljIHRvIG91ciBwcm9ibGVtLgotIGBpbnB1dF9zaGFwZWA6IFRoZSBzaGFwZSBvZiBvdXIgbnB1dHMgKDE1MHgxNTAgcGl4ZWwgaW1hZ2VzIHcvMyBjb2xvciBjaGFubmVscykuCgpfX1RpcF9fOiBDaGVjayBvdXQgdGhlIFt0Zmh1Yl0oaHR0cHM6Ly9naXRodWIuY29tL3JzdHVkaW8vdGZodWIpIHBhY2thZ2Ugd2hpY2gKbWFrZXMgaXQgZWFzeSB0byBpbnRlcmFjdCB3aXRoIFtUZW5zb3JGbG93IEh1Yl0oaHR0cHM6Ly93d3cudGVuc29yZmxvdy5vcmcvaHViKSwKYSBsaWJhcnJ5IGZvciBwdWJsaWNhdGlvbiwgZGlzY292ZXJ5LCBhbmQgY29uc3VtcHRpb24gb2YgcmVzdXNhYmxlIG1vZGVscy4KCmBgYHtyIHByZXRyYWluZWQtbW9kZWx9CmNvbnZfYmFzZSA8LSBhcHBsaWNhdGlvbl92Z2cxNigKICB3ZWlnaHRzID0gImltYWdlbmV0IiwKICBpbmNsdWRlX3RvcCA9IEZBTFNFLAogIGlucHV0X3NoYXBlID0gYygxNTAsIDE1MCwgMykKKQpgYGAKCmBgYHtyIHZnZzE2LW1vZGVsLXN0cnVjdHVyZX0Kc3VtbWFyeShjb252X2Jhc2UpCmBgYAoKIyMgRXh0cmFjdGluZyBmZWF0dXJlcyB1c2luZyB0aGUgcHJldHJhaW5lZCBjb252b2x1dGlvbmFsIGJhc2UKClRoaXMgc2VlbXMgYSBsaXR0bGUgZGF1bnRpbmcgdG8gdW5kZXJzdGFuZCBidXQgdGhpcyBpcyBqdXN0IGltcGxlbWVudGluZyBtb3JlIG9mIAphIG1hbnVhbCBhcHByb2FjaCB0byB3aGF0IHlvdSBhbHJlYWR5IGhhdmUgYmVlbiBkb2luZy4gSGVyZSB3ZSBjcmVhdGUgYSBmdW5jdGlvbiAKdGhhdCB3aWxsOgoKMS4gQ3JlYXRlIGFuIGVtcHR5IHRlbnNvciB0byBob2xkIG91ciB0cmFuc2Zvcm1lZCBmZWF0dXJlcyBhbmQgbGFiZWxzLgoyLiBDcmVhdGUgb3VyIGRhdGEgYmF0Y2ggaW1wb3J0aW5nIGdlbmVyYXRvci4gSGVyZSB3ZSB3aWxsIHVzZSBiYXRjaCBzaXplIG9mIDIwIAogICBzaW1wbHkgYmVjYXVzZSB3ZSBoYXZlIDIwMDAgdHJhaW5pbmcgaW1hZ2VzIGFuZCAxMDAwIHZhbGlkYXRpb24gaW1hZ2VzIGFuZCAKICAgYSBiYXRjaCBzaXplIG9mIDIwIGRpdmlkZXMgbmljZWx5IHRvIGJvdGggb2YgdGhlc2UgdmFsdWVzLgozLiBMb29wIHRocm91Z2ggb3VyIHRyYWluaW5nIGRhdGE6CiAgIGEuIGltcG9ydCBhIGJhdGNoIG9mIGltYWdlcwogICBiLiBhcHBseSB0aGUgcHJldHJhaW5lZCBiYXNlIHRvIG91ciBpbWFnZXMgdG8gb3V0cHV0IHRoZSBwcmVkaWN0ZWQgZmVhdHVyZXMKICAgYy4gYWRkIHRoZXNlIG5ldyBmZWF0dXJlcyBhbmQgdGhlIGxhYmVscyB0byBvdXIgdGVuc29yCiAgIGQuIGNvbnRpbnVlIHRvIGRvIHRoaXMgdW50aWwgb3VyIG51bWJlciBvZiBpdGVyYXRpb25zIHggYmF0Y2ggc2l6ZSBlcXVhbHMgb3IgCiAgICAgIGV4Y2VlZHMgb3VyIHRvdGFsIG51bWJlciBvZiBzYW1wbGVzLgoKYGBge3IgaW1hZ2UtZ2VuZXJhdG9yLWZlYXR1cmUtZXh0cmFjdGlvbn0KCmRhdGFnZW4gPC0gaW1hZ2VfZGF0YV9nZW5lcmF0b3IocmVzY2FsZSA9IDEvMjU1KQpiYXRjaF9zaXplIDwtIDIwCgpleHRyYWN0X2ZlYXR1cmVzIDwtIGZ1bmN0aW9uKGRpcmVjdG9yeSwgc2FtcGxlX2NvdW50KSB7CiAgCiAgZmVhdHVyZXMgPC0gYXJyYXkoMCwgZGltID0gYyhzYW1wbGVfY291bnQsIDQsIDQsIDUxMikpICAgIyBzdGVwIDEKICBsYWJlbHMgPC0gYXJyYXkoMCwgZGltID0gYyhzYW1wbGVfY291bnQpKSAgICAgICAgICAgICAgICAjIHN0ZXAgMQogIAogIGdlbmVyYXRvciA8LSBmbG93X2ltYWdlc19mcm9tX2RpcmVjdG9yeSggICAgICAgICAgICAgICAgICMgc3RlcCAyCiAgICBkaXJlY3RvcnkgPSBkaXJlY3RvcnksICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICBnZW5lcmF0b3IgPSBkYXRhZ2VuLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICB0YXJnZXRfc2l6ZSA9IGMoMTUwLCAxNTApLAogICAgYmF0Y2hfc2l6ZSA9IGJhdGNoX3NpemUsCiAgICBjbGFzc19tb2RlID0gImJpbmFyeSIKICApCiAgCiAgaSA8LSAwCiAgd2hpbGUgKFRSVUUpIHsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBzdGVwIDMKICAgIG1lc3NhZ2UoIlByb2Nlc3NpbmcgYmF0Y2ggIiwgaSArIDEsICIgb2YgIiwgY2VpbGluZyhzYW1wbGVfY291bnQgLyBiYXRjaF9zaXplKSkKICAgIGJhdGNoIDwtIGdlbmVyYXRvcl9uZXh0KGdlbmVyYXRvcikgICAgICAgICAgICAgICAgICAgICAjIHN0ZXAgM2EKICAgIGlucHV0c19iYXRjaCA8LSBiYXRjaFtbMV1dCiAgICBsYWJlbHNfYmF0Y2ggPC0gYmF0Y2hbWzJdXQogICAgZmVhdHVyZXNfYmF0Y2ggPC0gY29udl9iYXNlICU+JSBwcmVkaWN0KGlucHV0c19iYXRjaCkgICMgc3RlcCAzYgogICAgaW5kZXhfcmFuZ2UgPC0gKChpICogYmF0Y2hfc2l6ZSkgKyAxKTooKGkgKyAxKSAqIGJhdGNoX3NpemUpCiAgICBmZWF0dXJlc1tpbmRleF9yYW5nZSwsLF0gPC0gZmVhdHVyZXNfYmF0Y2ggICAgICAgICAgICAgIyBzdGVwIDNjCiAgICBsYWJlbHNbaW5kZXhfcmFuZ2VdIDwtIGxhYmVsc19iYXRjaCAgICAgICAgICAgICAgICAgICAgIyBzdGVwIDNjCiAgICBpIDwtIGkgKyAxCiAgICBpZiAoaSAqIGJhdGNoX3NpemUgPj0gc2FtcGxlX2NvdW50KSBicmVhayAgICAgICAgICAgICAgIyBzdGVwIDNkCiAgfQogIAogIGxpc3QoCiAgICBmZWF0dXJlcyA9IGZlYXR1cmVzLAogICAgbGFiZWxzID0gbGFiZWxzCiAgKSAKfQpgYGAKCiBMZXQncyBhcHBseSB0aGlzIGZ1bmN0aW9uIHRvIG91ciB0cmFpbmluZywgdmFsaWRhdGlvbiwgYW5kIHRlc3QgZGF0YQogCiAqKldpdGhvdXQgYSBHUFUgdGhpcyB3aWxsIHRha2UgYXBwcm94aW1hdGVseSA1IG1pbnV0ZXMgdG8gZXhlY3V0ZSoqCiAKYGBge3IgZXhlY3V0ZS1mZWF0dXJlLWV4dHJhY3Rpb259CnRyYWluIDwtIGV4dHJhY3RfZmVhdHVyZXModHJhaW5fZGlyLCAyMDAwKQp2YWxpZGF0aW9uIDwtIGV4dHJhY3RfZmVhdHVyZXModmFsaWRfZGlyLCAxMDAwKQp0ZXN0IDwtIGV4dHJhY3RfZmVhdHVyZXModGVzdF9kaXIsIDEwMDApCmBgYAoKIyMgUmVzaGFwZSBmZWF0dXJlcwoKVGhlIGV4dHJhY3RlZCBmZWF0dXJlcyB3aWxsIGJlIGEgNEQgdGVuc29yIChzYW1wbGVzLCA0LCA0LCA1MTIpLiAgV2UgY2FuIHNlZSB0aGlzIAppbiB0aGUgbGFzdCBsYXllciBvZiBvdXIgYGNvbnZfYmFzZWAgbW9kZWwgYWJvdmUgKGBibG9jazVfcG9vbCAoTWF4UG9vbGluZzJEKWApLiAKCkNvbnNlcXVlbnRseSwgd2UgbmVlZCB0byByZXNoYXBlIChmbGF0dGVuKSB0aGVzZSBpbnRvIGEgMkQgdGVuc29yIHRvIGZlZWQgaW50byAKYSBkZW5zZWx5IGNvbm5lY3RlZCBjbGFzc2lmaWVyLiBUaGlzIHJlc3VsdHMgaW4gYSAyRCB0ZW5zb3Igb2Ygc2l6ZSAKKHNhbXBsZXMsIDQgKiA0ICogNTEyID0gODE5MikuCgpgYGB7ciByZXNoYXBlLWZlYXR1cmVzfQpyZXNoYXBlX2ZlYXR1cmVzIDwtIGZ1bmN0aW9uKGZlYXR1cmVzKSB7CiAgYXJyYXlfcmVzaGFwZShmZWF0dXJlcywgZGltID0gYyhucm93KGZlYXR1cmVzKSwgNCAqIDQgKiA1MTIpKQp9Cgp0cmFpbiRmZWF0dXJlcyA8LSByZXNoYXBlX2ZlYXR1cmVzKHRyYWluJGZlYXR1cmVzKQp2YWxpZGF0aW9uJGZlYXR1cmVzIDwtIHJlc2hhcGVfZmVhdHVyZXModmFsaWRhdGlvbiRmZWF0dXJlcykKdGVzdCRmZWF0dXJlcyA8LSByZXNoYXBlX2ZlYXR1cmVzKHRlc3QkZmVhdHVyZXMpCgpkaW0odHJhaW4kZmVhdHVyZXMpCmBgYAoKIyMgRGVmaW5lIGRlbnNlbHkgY29ubmVjdGVkIGNsYXNzaWZpZXIKCldlJ3ZlIGV4dHJhY3RlZCBhbmQgZmxhdHRlbmVkIG91ciBmZWF0dXJlcyBmcm9tIHRoZSBjb252b2x1dGlvbiBsYXllcnMgc28gbm93CndlIG9ubHkgbmVlZCB0byBidWlsZCB0aGUgZGVuc2VseSBjb25uZWN0ZWQgY2xhc3NpZmllciBwb3J0aW9uIG9mIG91ciBtb2RlbC4KCmBgYHtyIG1vZGVsLWNsYXNzaWZpZXJ9Cm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDI1NiwgYWN0aXZhdGlvbiA9ICJyZWx1IiwgaW5wdXRfc2hhcGUgPSA0ICogNCAqIDUxMikgJT4lCiAgbGF5ZXJfZHJvcG91dChyYXRlID0gMC41KSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEsIGFjdGl2YXRpb24gPSAic2lnbW9pZCIpCgpzdW1tYXJ5KG1vZGVsKQpgYGAKCk5vdyB3ZSBjYW4gY29tcGlsZSBhbmQgdHJhaW4gb3VyIG1vZGVsLiBUaGlzIHdpbGwgdHJhaW4gcXVpY2tseSwgdGFraW5nCmFwcHJveGltYXRlbHkgMSBtaW4gd2hlbiB0cmFpbmVkIG9uIHlvdXIgbG9jYWwgQ1BVLiBPdXIgdmFsaWRhdGlvbiBsb3NzIGFsc28KaW1wcm92ZXMgb3ZlciB0aGUgcHJldmlvdXMgQ05OIHdlIGJ1aWx0LgoKYGBge3IgdHJhaW4tbW9kZWx9Cm1vZGVsICU+JSBjb21waWxlKAogIG9wdGltaXplciA9IG9wdGltaXplcl9ybXNwcm9wKGxyID0gMC4wMDAxKSwKICBsb3NzID0gImJpbmFyeV9jcm9zc2VudHJvcHkiLAogIG1ldHJpY3MgPSBjKCJhY2N1cmFjeSIpCikKCmhpc3RvcnkxIDwtIG1vZGVsICU+JSBmaXQoCiAgdHJhaW4kZmVhdHVyZXMsIHRyYWluJGxhYmVscywKICBlcG9jaHMgPSAzMCwKICBiYXRjaF9zaXplID0gMzIsCiAgdmFsaWRhdGlvbl9kYXRhID0gbGlzdCh2YWxpZGF0aW9uJGZlYXR1cmVzLCB2YWxpZGF0aW9uJGxhYmVscyksCiAgY2FsbGJhY2tzID0gbGlzdCgKICAgIGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMTApLAogICAgY2FsbGJhY2tfcmVkdWNlX2xyX29uX3BsYXRlYXUocGF0aWVuY2UgPSAyKQogICkKKQpgYGAKClNvIHdlIGFjaGVpdmVkIGEgc2lnbmlmaWNhbnQgZGVjcmVhc2UgaW4gb3VyIGxvc3Mgc2NvcmUsIGluY3JlYXNlZCBvdXIgYWNjdXJhY3kgCnRvIDkwJSwgYW5kIGRpZCBzbyBpbiBhIGZyYWN0aW9uIG9mIHRoZSB0aW1lIQoKYGBge3Igc2Vjb25kLW1vZGVsLXJlc3VsdHN9CmJlc3RfZXBvY2ggPC0gd2hpY2gubWluKGhpc3RvcnkxJG1ldHJpY3MkdmFsX2xvc3MpCmJlc3RfbG9zcyA8LSBoaXN0b3J5MSRtZXRyaWNzJHZhbF9sb3NzW2Jlc3RfZXBvY2hdICU+JSByb3VuZCgzKQpiZXN0X2FjYyA8LSBoaXN0b3J5MSRtZXRyaWNzJHZhbF9hY2N1cmFjeVtiZXN0X2Vwb2NoXSAlPiUgcm91bmQoMykKCmdsdWUoIk91ciBvcHRpbWFsIGxvc3MgaXMge2Jlc3RfbG9zc30gd2l0aCBhbiBhY2N1cmFjeSBvZiB7YmVzdF9hY2N9IikKYGBgCgpgYGB7ciBwbG90LW1vZGVsMS1oaXN0b3J5fQpwbG90KGhpc3RvcnkxKSArIAogIHNjYWxlX3hfY29udGludW91cyhsaW1pdHMgPSBjKDAsIGxlbmd0aChoaXN0b3J5MSRtZXRyaWNzJHZhbF9sb3NzKSkpCmBgYAoKCiMgIFRyYW5zZmVyIExlYXJuaW5nOiBFbmQtdG8tRW5kCgrimqDvuI/imqDvuI8gT05MWSBSVU4gT04gR1BVISEg4pqg77iP4pqg77iPCgpUaGUgYWJvdmUgYXBwcm9hY2ggcGVyZm9ybWVkIHByZXR0eSB3ZWxsLiBIb3dldmVyLCB3ZSBjYW4gc2VlIHRoYXQgd2UgYXJlIHN0aWxsIApvdmVyZml0dGluZywgd2hpY2ggbWF5IGJlIHJlZHVjaW5nIG1vZGVsIHBlcmZvcm1hbmNlLiBBbiBhbHRlcm5hdGl2ZSBhcHByb2FjaCBpcyAKdG8gcnVuIGEgcHJldHJhaW5lZCBtb2RlbCBmcm9tIGVuZC10by1lbmQuIFRoaXMgYXBwcm9hY2ggaXMgbXVjaCBzbG93ZXIgYW5kIApjb21wdXRhdGlvbmFsbHkgaW50ZW5zZTsgaG93ZXZlciwgaXQgb2ZmZXJzIGdyZWF0ZXIgZmxleGliaWxpdHkgaW4gdXNpbmcgYW5kIAphZGp1c3RpbmcgdGhlIHByZXRyYWluZWQgbW9kZWwgYmVjYXVzZSBpdCBsZXRzIHlvdToKCjEuIHVzZSBkYXRhIGF1Z21lbnRhdGlvbiB0byBkZWNyZWFzZSBvdmVyZml0dGluZyAoYW5kIHVzdWFsbHkgaW5jcmVhc2UgbW9kZWwgCiAgIHBlcmZvcm1hbmNlKS4KMi4gZmluZSB0dW5lIHBhcnRzIG9mIHRoZSBwcmV0cmFpbmVkIG1vZGVsLgoKVGhlIGZvbGxvd2luZyBhcHByb2FjaCBzaW1wbHkgcGx1Z3MgdGhlIHByZXRyYWluZWQgY29udm9sdXRpb24gYmFzZSBpbnRvIGEgCnNlcXVlbnRpYWwgbW9kZWwgYnV0IF9fX2ZyZWV6ZXNfX18gdGhlIGNvbnZvbHV0aW9uIGJhc2Ugd2VpZ2h0cy4KCiMjIENvbWJpbmluZyBhIGRlbnNlbHktY29ubmVjdGVkIG5ldXJhbCBuZXR3b3JrIHdpdGggdGhlIGNvbnZvbHV0aW9uYWwgYmFzZQoKSW4gdGhpcyBjYXNlIHdlIGNhbiBsaXRlcmFsbHkgcGx1ZyBpbiBvdXIgYGNvbnZfYmFzZWAgd2l0aGluIG91ciBtb2RlbCAKYXJjaGl0ZWN0dXJlLiAKCmBgYHtyIENOTi1iYXNlLWFuZC1jbGFzc2lmaWVyfQoKbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQogIGNvbnZfYmFzZSAlPiUKICBsYXllcl9mbGF0dGVuKCkgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAyNTYsIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSwgYWN0aXZhdGlvbiA9ICJzaWdtb2lkIikKCm1vZGVsCmBgYAoKIyMgRnJlZXppbmcgbGF5ZXJzCgpCZWZvcmUgeW91IGNvbXBpbGUgYW5kIHRyYWluIHRoZSBtb2RlbCwgaXQncyBpbXBvcnRhbnQgdG8gZnJlZXplIHRoZSBjb252b2x1dGlvbmFsIApiYXNlIHdlaWdodHMuIFRoaXMgcHJldmVudHMgdGhlIHdlaWdodHMgZnJvbSBiZWluZyB1cGRhdGVkIGR1cmluZyB0cmFpbmluZy4gSWYgCnlvdSBkb24ndCBkbyB0aGlzIHRoZW4gdGhlIHJlcHJlc2VudGF0aW9ucyBmb3VuZCBpbiB0aGUgcHJldHJhaW5lZCBtb2RlbCB3aWxsIGJlIAptb2RpZmllZCBhbmQsIHBvdGVudGlhbGx5LCBjb21wbGV0ZWx5IGRlc3Ryb3llZC4KCmBgYHtyIGZyZWV6ZS1wYXJhbWV0ZXJzfQpjYXQobGVuZ3RoKG1vZGVsJHRyYWluYWJsZV93ZWlnaHRzKSwgInRyYWluYWJsZSB3ZWlnaHQgdGVuc29ycyBiZWZvcmUgZnJlZXppbmcuXG4iKQoKZnJlZXplX3dlaWdodHMoY29udl9iYXNlKQoKY2F0KGxlbmd0aChtb2RlbCR0cmFpbmFibGVfd2VpZ2h0cyksICJ0cmFpbmFibGUgd2VpZ2h0IHRlbnNvcnMgYmVmb3JlIGZyZWV6aW5nLlxuIikKYGBgCgojIyBUcmFpbmluZyB0aGUgbW9kZWwgZW5kLXRvLWVuZCB3aXRoIGEgZnJvemVuIGNvbnZvbHV0aW9uYWwgYmFzZQoKVGhlIGZvbGxvd2luZyB0cmFpbnMgdGhlIG1vZGVsIGVuZC10by1lbmQgdXNpbmcgYWxsIENOTiBsb2dpYyB0aGF0IHlvdSBoYXZlIHNlZW4gCmJlZm9yZToKCjEuIGRhdGEgYXVnbWVudGF0aW9uCjIuIGltYWdlIGdlbmVyYXRvcgozLiBjb21waWxlIG91ciBtb2RlbAo0LiB0cmFpbiBvdXIgbW9kZWwKCmBgYHtyIHRyYWluLWVuZC10by1lbmR9Cgp0cmFpbl9kYXRhZ2VuID0gaW1hZ2VfZGF0YV9nZW5lcmF0b3IoCiAgcmVzY2FsZSA9IDEvMjU1LAogIHJvdGF0aW9uX3JhbmdlID0gNDAsCiAgd2lkdGhfc2hpZnRfcmFuZ2UgPSAwLjIsCiAgaGVpZ2h0X3NoaWZ0X3JhbmdlID0gMC4yLAogIHNoZWFyX3JhbmdlID0gMC4yLAogIHpvb21fcmFuZ2UgPSAwLjIsCiAgaG9yaXpvbnRhbF9mbGlwID0gVFJVRSwKICBmaWxsX21vZGUgPSAibmVhcmVzdCIKKQoKdGVzdF9kYXRhZ2VuIDwtIGltYWdlX2RhdGFfZ2VuZXJhdG9yKHJlc2NhbGUgPSAxLzI1NSkKCnRyYWluX2dlbmVyYXRvciA8LSBmbG93X2ltYWdlc19mcm9tX2RpcmVjdG9yeSgKICB0cmFpbl9kaXIsCiAgdHJhaW5fZGF0YWdlbiwKICB0YXJnZXRfc2l6ZSA9IGMoMTUwLCAxNTApLAogIGJhdGNoX3NpemUgPSAyMCwKICBjbGFzc19tb2RlID0gImJpbmFyeSIKKQoKdmFsaWRhdGlvbl9nZW5lcmF0b3IgPC0gZmxvd19pbWFnZXNfZnJvbV9kaXJlY3RvcnkoCiAgdmFsaWRhdGlvbl9kaXIsCiAgdGVzdF9kYXRhZ2VuLAogIHRhcmdldF9zaXplID0gYygxNTAsIDE1MCksCiAgYmF0Y2hfc2l6ZSA9IDIwLAogIGNsYXNzX21vZGUgPSAiYmluYXJ5IgopCgptb2RlbCAlPiUgY29tcGlsZSgKICBsb3NzID0gImJpbmFyeV9jcm9zc2VudHJvcHkiLAogIG9wdGltaXplciA9IG9wdGltaXplcl9ybXNwcm9wKGxyID0gMWUtNSksCiAgbWV0cmljcyA9IGMoImFjY3VyYWN5IikKKQoKaGlzdG9yeTIgPC0gbW9kZWwgJT4lIGZpdF9nZW5lcmF0b3IoCiAgdHJhaW5fZ2VuZXJhdG9yLAogIHN0ZXBzX3Blcl9lcG9jaCA9IDEwMCwKICBlcG9jaHMgPSAzMCwKICB2YWxpZGF0aW9uX2RhdGEgPSB2YWxpZGF0aW9uX2dlbmVyYXRvciwKICB2YWxpZGF0aW9uX3N0ZXBzID0gNTAKKQoKYGBgCgpgYGB7ciBwbG90LW1vZGVsMi1oaXN0b3J5fQpwbG90KGhpc3RvcnkyKQpgYGAKCgojICBUcmFuc2ZlciBMZWFybmluZzogRmluZSBUdW5lCgpBbm90aGVyIHdpZGVseSB1c2VkIHRlY2huaXF1ZSBmb3IgdXNpbmcgcHJldHJhaW5lZCBtb2RlbHMsIGlzIHRvIHVuZnJlZXplIGEgZmV3IApvZiB0aGUgY29udm9sdXRpb25hbCBiYXNlIGFuZCBhbGxvdyB0aG9zZSB3ZWlnaHRzIHRvIGJlIHVwZGF0ZWQuIFJlY2FsbCB0aGF0IAp0aGUgZWFybHkgbGF5ZXJzIGluIGEgQ05OIGlkZW50aWZ5IGRldGFpbGVkIGVkZ2VzIGFuZCBzaGFwZXMuIExhdGVyIGxheWVycyBwdXQgCnRoZXNlIGVkZ2VzIGFuZCBzaGFwZXMgdG9nZXRoZXIgdG8gbWFrZSBoaWdoZXIgb3JkZXIgcGFydHMgb2YgdGhlIGltYWdlcyB3ZSBhcmUgCnRyeWluZyB0byBjbGFzc2lmeSAoaS5lLiBjYXQgZWFycywgZG9nIHRhaWxzKS4gCgpUaGUgbW9yZSBvdXIgaW1hZ2VzIGRldmlhdGUgZnJvbSB0aGUgaW1hZ2VzIHVzZWQgdG8gY3JlYXRlIHRoZSBwcmV0cmFpbmVkIG1vZGVsLCAKdGhlbiB0aGUgbW9yZSBsaWtlbHkgeW91IHdpbGwgd2FudCB0byByZXRyYWluIHRoZSBsYXN0IGZldyBsYXllcnMsIHdoaWNoIHdpbGwgCm1ha2UgdGhlIGVkZ2UgYW5kIHNoYXBlIGZlYXR1cmVzIG1vcmUgcmVsZXZhbnQgdG8geW91ciBwcm9ibGVtLgoKVG8gZmluZS10dW5lIGEgcHJldHJhaW5lZCBtb2RlbCB5b3U6CgoxLiBBZGQgeW91ciBjdXN0b20gbmV0d29yayBvbiB0b3Agb2YgYW4gYWxyZWFkeS10cmFpbmVkIGJhc2UgbmV0d29yayAoZXhlY3V0ZWQgCiAgIGluIHRoZSBgQ05OLWJhc2UtYW5kLWNsYXNzaWZpZXJgIGNvZGUgY2h1bmspLgoyLiBGcmVlemUgdGhlIGJhc2UgbmV0d29yayAoZXhlY3V0ZWQgaW4gdGhlIGBmcmVlemUtcGFyYW1ldGVyc2AgY29kZSBjaHVuaykuCjMuIFRyYWluIHRoZSBwYXJ0IHlvdSBhZGRlZCAoZXhlY3V0ZWQgaW4gdGhlIGB0cmFpbi1lbmQtdG8tZW5kYCBjb2RlIGNodW5rKS4KNC4gVW5mcmVlemUgc29tZSBsYXllcnMgaW4gdGhlIGJhc2UgbmV0d29yay4KNS4gSm9pbnRseSB0cmFpbiBib3RoIHRoZXNlIGxheWVycyBhbmQgdGhlIHBhcnQgeW91IGFkZGVkLgoKV2UgYWxyZWFkeSBkaWQgc3RlcHMgMS0zLiBUaGUgZm9sbG93aW5nIGV4ZWN1dGVzIHN0ZXBzIDQgYW5kIDUuCgojIyBVbmZyZWV6ZSBzb21lIGxheWVycyBpbiB0aGUgYmFzZSBuZXR3b3JrCgpgYGB7ciB1bmZyZWV6ZS1zb21lLWxheWVyc30KdW5mcmVlemVfd2VpZ2h0cyhjb252X2Jhc2UsIGZyb20gPSAiYmxvY2szX2NvbnYxIikKYGBgCgoKIyMgSm9pbnRseSB0cmFpbiBib3RoIHRoZXNlIGxheWVycyBhbmQgdGhlIHBhcnQgeW91IGFkZGVkCgpgYGB7ciBmaW5lLXR1bmUtbW9kZWx9Cm1vZGVsICU+JSBjb21waWxlKAogIGxvc3MgPSAiYmluYXJ5X2Nyb3NzZW50cm9weSIsCiAgb3B0aW1pemVyID0gb3B0aW1pemVyX3Jtc3Byb3AobHIgPSAxZS01KSwKICBtZXRyaWNzID0gYygiYWNjdXJhY3kiKQopCgpoaXN0b3J5MiA8LSBtb2RlbCAlPiUgZml0X2dlbmVyYXRvcigKICB0cmFpbl9nZW5lcmF0b3IsCiAgc3RlcHNfcGVyX2Vwb2NoID0gMTAwLAogIGVwb2NocyA9IDEwMCwKICB2YWxpZGF0aW9uX2RhdGEgPSB2YWxpZGF0aW9uX2dlbmVyYXRvciwKICB2YWxpZGF0aW9uX3N0ZXBzID0gNTAKKQpgYGAKCmBgYHtyIHBsb3QtbW9kZWwzLWhpc3Rvcnl9CnBsb3QoaGlzdG9yeTIpCmBgYAoKIyBUYWtld2F5cwoKKiBQcmUtdHJhaW5lZCBtb2RlbHMgY2FuIGJlIGVmZmljaWVudCBhbmQgZWZmZWN0aXZlIGZvciBwcm9ibGVtcyB0aGF0IGFsaWduIHRvCmNvbW1vbiBjb21wdXRlciB2aXNpb24gKGFuZCBvdGhlcikgdGFza3MuCgoqIE1hbnkgcHJlLWV4aXN0aW5nIG1vZGVscyBleGlzdCBvbiBbVGVuc29yRmxvdyBIdWJdKGh0dHBzOi8vd3d3LnRlbnNvcmZsb3cub3JnL2h1YikKICBhbmQgYXJlIHdvcnRoIHJlc2VhcmNoaW5nLgoKKiBUaHJlZSBtYWluIGFwcHJvYWNoZXMgZXhpc3RzIGZvciB0cmFuc2ZlciBsZWFybmluZzsgdGhlIGRlY2lzaW9uIGRlcGVuZHMgb24KICB5b3VyIGRlc2lyZSB0byBtYXhpbWl6ZSBlZmZpY2llbmN5IChjb21wdXRlIHRpbWUpIHZzIGVmZmVjdGl2ZW5lc3MgKGFjY3VyYWN5KToKICAgLSBmZWF0dXJlIGV4dHJhY3Rpb24KICAgLSBlbmQtdG8tZW5kIHRyYWluaW5nIHdpdGggZXhpc3RpbmcgQ05OIHdlaWdodHMKICAgLSBlbmQtdG9lbmQgdHJhaW5pbmcgd2hpbGUgZmluZS10dW5pbmcgbGF0dGVyIENOTiBsYXllciB3ZWlnaHRzCgpb8J+PoF0oaHR0cHM6Ly9naXRodWIuY29tL3JzdHVkaW8tY29uZi0yMDIwL2RsLWtlcmFzLXRmKQ==