\[ \newcommand{addtorrent}{\text{AddTorrent}} \]

library(tidyverse)
library(jsonlite)

1 Experiment Parameters

EXPERIMENT <- '10-network-4-seeders-4-seeder_sets-100MB-filesize'
experiment_file <- function(filename) file.path(EXPERIMENT, 'parsed', filename)

The torrent piece size is set at torrent creation time by torrentool.

PIECE_SIZE <- 262144
experiment_meta <- jsonlite::read_json(experiment_file('deluge_experiment_config_log_entry.jsonl'))
n_pieces <- experiment_meta$file_size / PIECE_SIZE
cat(paste0("File has ", rlang::as_bytes(experiment_meta$file_size), " and ", n_pieces, " pieces (blocks)."))

File has 104.86 MB and 400 pieces (blocks).

n_leechers <- length(experiment_meta$nodes$nodes) - experiment_meta$seeders
cat(paste0("Network has ", length(experiment_meta$nodes$nodes), " nodes with ", experiment_meta$seeders, " seeders and ", n_leechers, " leechers."))

Network has 10 nodes with 4 seeders and 6 leechers.

2 Logs

Read and extract run id and seed set from the dataset name.

downloads <- read_csv(
  experiment_file('deluge_torrent_download.csv'), 
  show_col_types = FALSE,
) |>
  mutate(
    temp = str_remove(torrent_name, '^dataset-'),
    seed_set = as.numeric(str_extract(temp, '^\\d+')),
    run = as.numeric(str_extract(temp, '\\d+$'))
  ) |>
  rename(piece = value) |>
  select(-temp, -name)

Since what we get are piece indices and they might be out of order, we need to actually count how many pieces were downloaded by the node up until a given instant:

downloads <- downloads |> 
  group_by(node, seed_set, run) |>
  arrange(timestamp) |>
  mutate(
    piece_count = seq_along(timestamp)
  ) |>
  ungroup() |>
  mutate(completed = piece_count / n_pieces)

We can have a brief look at the data to see that it makes sense.

ggplot(downloads |> 
         filter(seed_set < 3) |> 
         group_by(seed_set, run) |>
         mutate(timestamp = as.numeric(timestamp - min(timestamp))) |>
         ungroup()) +
  geom_line(aes(x = timestamp, y = completed, col = node), lwd=0.7) +
  scale_y_continuous(labels = scales::percent_format()) +
  facet_grid(run ~ seed_set, labeller = labeller(
    run = as_labeller(function(x) paste0("run: ", x)),
    seed_set = as_labeller(function(x) paste0("seed set: ", x)))) +
  xlab('elapsed time (seconds)') +
  ylab('download completion (%)') + 
  theme_bw(base_size = 15)

As we can see, the data seems to make sense. To the left we see the “download times” for seeders, which is almost instantaneous, followed by the downloads for the leechers. We see some variability across experiments, with some nodes seemingly struggling to complete their downloads at times.

3 Results

3.1 Sanity Checks

Have any nodes failed to download the entire file?

downloads |> 
  group_by(node, seed_set, run) |> 
  count() |> 
  ungroup() |> 
  filter(n != n_pieces)

Do we have as many runs and seed sets as we expect?

downloads |> 
  select(seed_set, node, run) |>
  distinct() |>
  group_by(seed_set, node) |>
  count() |>
  filter(n != experiment_meta$repetitions)

3.2 Computing Download Times

We define the download time for a Deluge node \(d\) as the time elapsed from the client’s response to an \(\addtorrent\) request and the time at which the client reports having received the last piece of the downloaded file. Since seeders are already in possession of the file by construction, we only measure download times at leechers.

add_torrent_requests <- read_csv(
  experiment_file('request_event.csv'), show_col_types = FALSE)
download_start <- add_torrent_requests |> 
  select(-request_id) |>
  filter(name == 'leech', type == 'RequestEventType.end') |> 
   mutate(
    # We didn't log those on the runner side so I have to reconstruct them.
    run = rep(rep(
      1:experiment_meta$repetitions - 1, 
      each = n_leechers), times=experiment_meta$seeder_sets),
    seed_set = rep(
      1:experiment_meta$seeder_sets - 1, 
      each = n_leechers * experiment_meta$repetitions),
  ) |> 
  transmute(node = destination, run, seed_set, seed_request_time = timestamp)
download_times <- downloads |> 
  left_join(download_start, by = c('node', 'run', 'seed_set')) |>
  mutate(
    elapsed_download_time = as.numeric(timestamp - seed_request_time)
  ) |> 
  group_by(node, run, seed_set) |> 
  mutate(lookup_time = as.numeric(min(timestamp) - seed_request_time)) |>
  ungroup()

If we did this right, the elapsed download time can never be negative, and neither can the lookup time.

download_times |> filter(elapsed_download_time < 0 | lookup_time < 0)

We can now actually compute statistics on the download times.

download_time_stats <- download_times |> 
  filter(!is.na(elapsed_download_time)) |>
  group_by(piece_count, completed) |>
  summarise(
    mean = mean(elapsed_download_time),
    median = median(elapsed_download_time),
    max = max(elapsed_download_time),
    min = min(elapsed_download_time),
    p90 = quantile(elapsed_download_time, p = 0.9),
    p10 = quantile(elapsed_download_time, p = 0.1),
    .groups = 'drop'
  )
ggplot(download_time_stats) +
  geom_ribbon(aes(xmin = p10, xmax = p90, y = completed), 
              fill = scales::alpha('blue', 0.5), col = 'lightgray') + 
  geom_line(aes(x = median, y = completed)) +
  theme_minimal() +
  ylab("completion") +
  xlab("time (seconds)") +
  ggtitle(paste0('download time (Deluge, ',rlang::as_bytes(experiment_meta$file_size),' file)'))

LS0tCnRpdGxlOiAiRGVsdWdlIERvd25sb2FkIFRpbWVzIC0tIEV4cGxvcmF0b3J5IEFuYWx5c2lzIgpvdXRwdXQ6IAogIGJvb2tkb3duOjpodG1sX25vdGVib29rMjoKICAgIG51bWJlcl9zZWN0aW9uczogVFJVRQogICAgdG9jOiBUUlVFCi0tLQoKJCQKXG5ld2NvbW1hbmR7YWRkdG9ycmVudH17XHRleHR7QWRkVG9ycmVudH19CiQkCgpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0KbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkoanNvbmxpdGUpCmBgYAoKCiMjIEV4cGVyaW1lbnQgUGFyYW1ldGVycwoKYGBge3J9CkVYUEVSSU1FTlQgPC0gJzEwLW5ldHdvcmstNC1zZWVkZXJzLTQtc2VlZGVyX3NldHMtMTAwTUItZmlsZXNpemUnCmV4cGVyaW1lbnRfZmlsZSA8LSBmdW5jdGlvbihmaWxlbmFtZSkgZmlsZS5wYXRoKEVYUEVSSU1FTlQsICdwYXJzZWQnLCBmaWxlbmFtZSkKYGBgCgpUaGUgdG9ycmVudCBwaWVjZSBzaXplIGlzIHNldCBhdCB0b3JyZW50IGNyZWF0aW9uIHRpbWUgYnkgW3RvcnJlbnRvb2xdKGh0dHBzOi8vZ2l0aHViLmNvbS9pZGxlc2lnbi90b3JyZW50b29sL2Jsb2IvNWYzN2Q2ZGNjMzA0NzU4YmFlNDZkMDFjNjNlNWJlMGYwYTM0OGJmYy90b3JyZW50b29sL3RvcnJlbnQucHkjTDM1NCkuCmBgYHtyfQpQSUVDRV9TSVpFIDwtIDI2MjE0NApgYGAgCgpgYGB7cn0KZXhwZXJpbWVudF9tZXRhIDwtIGpzb25saXRlOjpyZWFkX2pzb24oZXhwZXJpbWVudF9maWxlKCdkZWx1Z2VfZXhwZXJpbWVudF9jb25maWdfbG9nX2VudHJ5Lmpzb25sJykpCmBgYAoKYGBge3IgcmVzdWx0cz0nYXNpcyd9Cm5fcGllY2VzIDwtIGV4cGVyaW1lbnRfbWV0YSRmaWxlX3NpemUgLyBQSUVDRV9TSVpFCmNhdChwYXN0ZTAoIkZpbGUgaGFzICIsIHJsYW5nOjphc19ieXRlcyhleHBlcmltZW50X21ldGEkZmlsZV9zaXplKSwgIiBhbmQgIiwgbl9waWVjZXMsICIgcGllY2VzIChibG9ja3MpLiIpKQpgYGAKCmBgYHtyIHJlc3VsdHM9J2FzaXMnfQpuX2xlZWNoZXJzIDwtIGxlbmd0aChleHBlcmltZW50X21ldGEkbm9kZXMkbm9kZXMpIC0gZXhwZXJpbWVudF9tZXRhJHNlZWRlcnMKY2F0KHBhc3RlMCgiTmV0d29yayBoYXMgIiwgbGVuZ3RoKGV4cGVyaW1lbnRfbWV0YSRub2RlcyRub2RlcyksICIgbm9kZXMgd2l0aCAiLCBleHBlcmltZW50X21ldGEkc2VlZGVycywgIiBzZWVkZXJzIGFuZCAiLCBuX2xlZWNoZXJzLCAiIGxlZWNoZXJzLiIpKQoKYGBgCgojIyBMb2dzCgpSZWFkIGFuZCBleHRyYWN0IHJ1biBpZCBhbmQgc2VlZCBzZXQgZnJvbSB0aGUgZGF0YXNldCBuYW1lLgoKYGBge3J9CmRvd25sb2FkcyA8LSByZWFkX2NzdigKICBleHBlcmltZW50X2ZpbGUoJ2RlbHVnZV90b3JyZW50X2Rvd25sb2FkLmNzdicpLCAKICBzaG93X2NvbF90eXBlcyA9IEZBTFNFLAopIHw+CiAgbXV0YXRlKAogICAgdGVtcCA9IHN0cl9yZW1vdmUodG9ycmVudF9uYW1lLCAnXmRhdGFzZXQtJyksCiAgICBzZWVkX3NldCA9IGFzLm51bWVyaWMoc3RyX2V4dHJhY3QodGVtcCwgJ15cXGQrJykpLAogICAgcnVuID0gYXMubnVtZXJpYyhzdHJfZXh0cmFjdCh0ZW1wLCAnXFxkKyQnKSkKICApIHw+CiAgcmVuYW1lKHBpZWNlID0gdmFsdWUpIHw+CiAgc2VsZWN0KC10ZW1wLCAtbmFtZSkKYGBgCgpTaW5jZSB3aGF0IHdlIGdldCBhcmUgcGllY2UgaW5kaWNlcyBhbmQgdGhleSBtaWdodCBiZSBvdXQgb2Ygb3JkZXIsIHdlIG5lZWQgdG8gYWN0dWFsbHkgY291bnQgaG93IG1hbnkgcGllY2VzIHdlcmUgZG93bmxvYWRlZCBieSB0aGUgbm9kZSB1cCB1bnRpbCBhIGdpdmVuIGluc3RhbnQ6CgpgYGB7cn0KZG93bmxvYWRzIDwtIGRvd25sb2FkcyB8PiAKICBncm91cF9ieShub2RlLCBzZWVkX3NldCwgcnVuKSB8PgogIGFycmFuZ2UodGltZXN0YW1wKSB8PgogIG11dGF0ZSgKICAgIHBpZWNlX2NvdW50ID0gc2VxX2Fsb25nKHRpbWVzdGFtcCkKICApIHw+CiAgdW5ncm91cCgpIHw+CiAgbXV0YXRlKGNvbXBsZXRlZCA9IHBpZWNlX2NvdW50IC8gbl9waWVjZXMpCmBgYCAKCldlIGNhbiBoYXZlIGEgYnJpZWYgbG9vayBhdCB0aGUgZGF0YSB0byBzZWUgdGhhdCBpdCBtYWtlcyBzZW5zZS4KCmBgYHtyIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD0xMH0KZ2dwbG90KGRvd25sb2FkcyB8PiAKICAgICAgICAgZmlsdGVyKHNlZWRfc2V0IDwgMykgfD4gCiAgICAgICAgIGdyb3VwX2J5KHNlZWRfc2V0LCBydW4pIHw+CiAgICAgICAgIG11dGF0ZSh0aW1lc3RhbXAgPSBhcy5udW1lcmljKHRpbWVzdGFtcCAtIG1pbih0aW1lc3RhbXApKSkgfD4KICAgICAgICAgdW5ncm91cCgpKSArCiAgZ2VvbV9saW5lKGFlcyh4ID0gdGltZXN0YW1wLCB5ID0gY29tcGxldGVkLCBjb2wgPSBub2RlKSwgbHdkPTAuNykgKwogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsKICBmYWNldF9ncmlkKHJ1biB+IHNlZWRfc2V0LCBsYWJlbGxlciA9IGxhYmVsbGVyKAogICAgcnVuID0gYXNfbGFiZWxsZXIoZnVuY3Rpb24oeCkgcGFzdGUwKCJydW46ICIsIHgpKSwKICAgIHNlZWRfc2V0ID0gYXNfbGFiZWxsZXIoZnVuY3Rpb24oeCkgcGFzdGUwKCJzZWVkIHNldDogIiwgeCkpKSkgKwogIHhsYWIoJ2VsYXBzZWQgdGltZSAoc2Vjb25kcyknKSArCiAgeWxhYignZG93bmxvYWQgY29tcGxldGlvbiAoJSknKSArIAogIHRoZW1lX2J3KGJhc2Vfc2l6ZSA9IDE1KQpgYGAKCkFzIHdlIGNhbiBzZWUsIHRoZSBkYXRhIHNlZW1zIHRvIG1ha2Ugc2Vuc2UuIFRvIHRoZSBsZWZ0IHdlIHNlZSB0aGUgImRvd25sb2FkIHRpbWVzIiBmb3Igc2VlZGVycywgd2hpY2ggaXMgYWxtb3N0IGluc3RhbnRhbmVvdXMsIGZvbGxvd2VkIGJ5IHRoZSBkb3dubG9hZHMgZm9yIHRoZSBsZWVjaGVycy4gV2Ugc2VlIHNvbWUgdmFyaWFiaWxpdHkgYWNyb3NzIGV4cGVyaW1lbnRzLCB3aXRoIHNvbWUgbm9kZXMgc2VlbWluZ2x5IHN0cnVnZ2xpbmcgdG8gY29tcGxldGUgdGhlaXIgZG93bmxvYWRzIGF0IHRpbWVzLiAKCiMjIFJlc3VsdHMKCiMjIyBTYW5pdHkgQ2hlY2tzCgpIYXZlIGFueSBub2RlcyBmYWlsZWQgdG8gZG93bmxvYWQgdGhlIGVudGlyZSBmaWxlPwoKYGBge3J9CmRvd25sb2FkcyB8PiAKICBncm91cF9ieShub2RlLCBzZWVkX3NldCwgcnVuKSB8PiAKICBjb3VudCgpIHw+IAogIHVuZ3JvdXAoKSB8PiAKICBmaWx0ZXIobiAhPSBuX3BpZWNlcykKYGBgCgpEbyB3ZSBoYXZlIGFzIG1hbnkgcnVucyBhbmQgc2VlZCBzZXRzIGFzIHdlIGV4cGVjdD8KCmBgYHtyfQpkb3dubG9hZHMgfD4gCiAgc2VsZWN0KHNlZWRfc2V0LCBub2RlLCBydW4pIHw+CiAgZGlzdGluY3QoKSB8PgogIGdyb3VwX2J5KHNlZWRfc2V0LCBub2RlKSB8PgogIGNvdW50KCkgfD4KICBmaWx0ZXIobiAhPSBleHBlcmltZW50X21ldGEkcmVwZXRpdGlvbnMpCmBgYAoKCiMjIyBDb21wdXRpbmcgRG93bmxvYWQgVGltZXMKCldlIGRlZmluZSB0aGUgX2Rvd25sb2FkIHRpbWVfIGZvciBhIERlbHVnZSBub2RlICRkJCBhcyB0aGUgdGltZSBlbGFwc2VkIGZyb20gdGhlIGNsaWVudCdzIHJlc3BvbnNlIHRvIGFuICRcYWRkdG9ycmVudCQgcmVxdWVzdCBhbmQgdGhlIHRpbWUgYXQgd2hpY2ggdGhlIGNsaWVudCByZXBvcnRzIGhhdmluZyByZWNlaXZlZCB0aGUgbGFzdCBwaWVjZSBvZiB0aGUgZG93bmxvYWRlZCBmaWxlLiBTaW5jZSBzZWVkZXJzIGFyZSBhbHJlYWR5IGluIHBvc3Nlc3Npb24gb2YgdGhlIGZpbGUgYnkgY29uc3RydWN0aW9uLCB3ZSBvbmx5IG1lYXN1cmUgZG93bmxvYWQgdGltZXMgYXQgX2xlZWNoZXJzXy4KCmBgYHtyfQphZGRfdG9ycmVudF9yZXF1ZXN0cyA8LSByZWFkX2NzdigKICBleHBlcmltZW50X2ZpbGUoJ3JlcXVlc3RfZXZlbnQuY3N2JyksIHNob3dfY29sX3R5cGVzID0gRkFMU0UpCmBgYAoKYGBge3J9CmRvd25sb2FkX3N0YXJ0IDwtIGFkZF90b3JyZW50X3JlcXVlc3RzIHw+IAogIHNlbGVjdCgtcmVxdWVzdF9pZCkgfD4KICBmaWx0ZXIobmFtZSA9PSAnbGVlY2gnLCB0eXBlID09ICdSZXF1ZXN0RXZlbnRUeXBlLmVuZCcpIHw+IAogICBtdXRhdGUoCiAgICAjIFdlIGRpZG4ndCBsb2cgdGhvc2Ugb24gdGhlIHJ1bm5lciBzaWRlIHNvIEkgaGF2ZSB0byByZWNvbnN0cnVjdCB0aGVtLgogICAgcnVuID0gcmVwKHJlcCgKICAgICAgMTpleHBlcmltZW50X21ldGEkcmVwZXRpdGlvbnMgLSAxLCAKICAgICAgZWFjaCA9IG5fbGVlY2hlcnMpLCB0aW1lcz1leHBlcmltZW50X21ldGEkc2VlZGVyX3NldHMpLAogICAgc2VlZF9zZXQgPSByZXAoCiAgICAgIDE6ZXhwZXJpbWVudF9tZXRhJHNlZWRlcl9zZXRzIC0gMSwgCiAgICAgIGVhY2ggPSBuX2xlZWNoZXJzICogZXhwZXJpbWVudF9tZXRhJHJlcGV0aXRpb25zKSwKICApIHw+IAogIHRyYW5zbXV0ZShub2RlID0gZGVzdGluYXRpb24sIHJ1biwgc2VlZF9zZXQsIHNlZWRfcmVxdWVzdF90aW1lID0gdGltZXN0YW1wKQpgYGAKCmBgYHtyfQpkb3dubG9hZF90aW1lcyA8LSBkb3dubG9hZHMgfD4gCiAgbGVmdF9qb2luKGRvd25sb2FkX3N0YXJ0LCBieSA9IGMoJ25vZGUnLCAncnVuJywgJ3NlZWRfc2V0JykpIHw+CiAgbXV0YXRlKAogICAgZWxhcHNlZF9kb3dubG9hZF90aW1lID0gYXMubnVtZXJpYyh0aW1lc3RhbXAgLSBzZWVkX3JlcXVlc3RfdGltZSkKICApIHw+IAogIGdyb3VwX2J5KG5vZGUsIHJ1biwgc2VlZF9zZXQpIHw+IAogIG11dGF0ZShsb29rdXBfdGltZSA9IGFzLm51bWVyaWMobWluKHRpbWVzdGFtcCkgLSBzZWVkX3JlcXVlc3RfdGltZSkpIHw+CiAgdW5ncm91cCgpCmBgYAoKSWYgd2UgZGlkIHRoaXMgcmlnaHQsIHRoZSBlbGFwc2VkIGRvd25sb2FkIHRpbWUgY2FuIG5ldmVyIGJlIG5lZ2F0aXZlLCBhbmQgbmVpdGhlciBjYW4gdGhlIGxvb2t1cCB0aW1lLgoKYGBge3J9CmRvd25sb2FkX3RpbWVzIHw+IGZpbHRlcihlbGFwc2VkX2Rvd25sb2FkX3RpbWUgPCAwIHwgbG9va3VwX3RpbWUgPCAwKQpgYGAKCldlIGNhbiBub3cgYWN0dWFsbHkgY29tcHV0ZSBzdGF0aXN0aWNzIG9uIHRoZSBkb3dubG9hZCB0aW1lcy4KCmBgYHtyfQpkb3dubG9hZF90aW1lX3N0YXRzIDwtIGRvd25sb2FkX3RpbWVzIHw+IAogIGZpbHRlcighaXMubmEoZWxhcHNlZF9kb3dubG9hZF90aW1lKSkgfD4KICBncm91cF9ieShwaWVjZV9jb3VudCwgY29tcGxldGVkKSB8PgogIHN1bW1hcmlzZSgKICAgIG1lYW4gPSBtZWFuKGVsYXBzZWRfZG93bmxvYWRfdGltZSksCiAgICBtZWRpYW4gPSBtZWRpYW4oZWxhcHNlZF9kb3dubG9hZF90aW1lKSwKICAgIG1heCA9IG1heChlbGFwc2VkX2Rvd25sb2FkX3RpbWUpLAogICAgbWluID0gbWluKGVsYXBzZWRfZG93bmxvYWRfdGltZSksCiAgICBwOTAgPSBxdWFudGlsZShlbGFwc2VkX2Rvd25sb2FkX3RpbWUsIHAgPSAwLjkpLAogICAgcDEwID0gcXVhbnRpbGUoZWxhcHNlZF9kb3dubG9hZF90aW1lLCBwID0gMC4xKSwKICAgIC5ncm91cHMgPSAnZHJvcCcKICApCmBgYAoKCmBgYHtyfQpnZ3Bsb3QoZG93bmxvYWRfdGltZV9zdGF0cykgKwogIGdlb21fcmliYm9uKGFlcyh4bWluID0gcDEwLCB4bWF4ID0gcDkwLCB5ID0gY29tcGxldGVkKSwgCiAgICAgICAgICAgICAgZmlsbCA9IHNjYWxlczo6YWxwaGEoJ2JsdWUnLCAwLjUpLCBjb2wgPSAnbGlnaHRncmF5JykgKyAKICBnZW9tX2xpbmUoYWVzKHggPSBtZWRpYW4sIHkgPSBjb21wbGV0ZWQpKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICB5bGFiKCJjb21wbGV0aW9uIikgKwogIHhsYWIoInRpbWUgKHNlY29uZHMpIikgKwogIGdndGl0bGUocGFzdGUwKCdkb3dubG9hZCB0aW1lIChEZWx1Z2UsICcscmxhbmc6OmFzX2J5dGVzKGV4cGVyaW1lbnRfbWV0YSRmaWxlX3NpemUpLCcgZmlsZSknKSkKYGBgCgo=