Friday, May 20, 2011

Импорт базы данных с сервера на Andoird

Эх, что-то давно я у себя в блоге ничего не писал...
Пора бы уже и поделиться чем-нибудь свеженьким.

В общем задача ( для меня свежая, для кого-то может и не очень ) :
а) есть сервер с базой данных
б) мобильное приложение, которое должно кэшировать часть серверной базы данных
в) доступ к данным возможен через веб сервис в виде csv / xml / json
Требуется реализовать импорт базы данных наиболее эфективным способом, поскольку данных достаточно много.

Итак, поехали ...

Первое с чего я решил начать, это выборка данных через веб сервис. В каком виде брать данные из веб сервиса ? Долго не думая над этим вопросо я решил пройти по протоптоной дорожке и вытаскивать данные в JSON. Почему ? Всё очень просто. Во-первых, Android SDK имеет встроенную поддержку JSON с помощью org.json библиотеки. Во-вторых, работать с JSON, как по мне, несколько удобнее чем с XML или CSV. И в третьих он компактнее чем XML, хотя и не настолько компактный как CSV.

В общем первая реализация не заставила себя долго ждать и получилось что-то вроде:
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(url.toString());
HttpResponse response = client.execute(request);

// Pull content stream from response
HttpEntity entity = response.getEntity();
InputStream inputStream = entity.getContent();

ByteArrayOutputStream content = new ByteArrayOutputStream();

// Read response into a buffered stream
int readBytes = 0;
byte[] buffer = new byte[BUFFER_SIZE];
while ((readBytes = inputStream.read(buffer)) != -1) {
  content.write(buffer, 0, readBytes);
}  

String plain = new String(content.toByteArray());
JSONArray data = new JSONArray(plain);
Отточеная до автоматизма тривиальность не предвещала никаких сложностей. Но увы, как это часто бывает всё не так просто как кажется на первый взгляд.

Пробный запуск и приложение вылетает с OutofMemoryError. Оказалось, что данных сервер выдаёт порядка 4,3Мб и при очередном копировании данных в памяти виртуальная машина выкидывает исключение. На каком именно копировании я не разбирался. Всего их получается три inputStream -> content, content -> plaint, plain -> data и в памяти в один момент могут держаться все четыре копии данных, что порядка 16Мб. Для меня всё же остаётся не совсем ясным почему возникает это исключение, поскольку на моём устройстве 576 Мб оперативки из которых 256 Мб обычно свободны. Ну да ладно. Зато это послужило хорошим поводом чтобы разобраться.

Первоё с чего я с коллегой начал - это постраничная выборка данных, так чтобы за один раз получать ограниченный набор данных и после его успешной обработки, тянуть следующий.

В итоге в приложение добавился цикл, а в url дополнительный параметр указывающий номер первой записи в наборе и размер страницы.

Попробовал - заработало. Ура !

Самое время заняться локальной базой данных и её наполнением. Вот примерный код:
for (int i = 0; i < response.length(); i++) {
  JSONObject item = response.getJSONObject(i).getJSONObject("item");
  Point point = Point.fromJson(item);
  ContentValues values = new ContentValues();
  values.put(KEY_POINT_ID, object.getId());
  values.put(KEY_POINT_NAME, object.getName());
  values.put(KEY_POINT_LAT, object.getLat());
  values.put(KEY_POINT_LON, object.getLon());
  values.put(KEY_POINT_TYPE, object.getType());
  values.put(KEY_POINT_UPDATE_TIME, object.getUpdateTime());
  db.insert(TABLE_POINT, null, values);
}
Запускаем - замечательно, всё работает как в сказке с первой попытки. Только вот как-то долго... Прошло минуты две, а обработаны меньше четверти данных... Вряд-ли у пользователей хватит терпения ждать 10 минут, пока загрузится приложение. Да и как-то не гуманно это что-ли, так что начинаем анализировать на что тратится так много времени. Для этого под Android есть специальная тулза traceview и Debug.startMethodTracing / Debug.stopMethodTracing в помощь. Важный момент - размер .trace - файла по умолчанию 8 Мб, что исчерпывается быстро. Поэтому я установил его побольше и ограничить импорт выборкой одной страницы в 582 записи. Оборачиваем интересующий нас код вот так:
Debug.startMethodTracing("myapp", 20 * 1024 * 1024);
// ... сдесь идёт код импорта данных ...
Debug.stopMethodTracing();
Снова запускаем и по завершению выполнения находим myapp.trace в корне /sdcard на телефоне. Теперь для анализа "сырых" данных из этого файла запускаем:
traceview <путь-к-папке>/myapp
Из таблички со статистикой видно что 77,5% времени "сжирает" операция вставки данных в таблицу. Затем 17,3% тратится на выборку данных с сервера из которых 14,4% от общего времени - это разборка JSON строк в объекты. Я решил начать оптимизировать с малого, а именно с разборки JSON. Первая мысль, которая мне пришла в голову - это использовать JSON Streaming. JSON Streaming - это аналог XML Pull парсера для JSON. Идея этого подхода заключается в том, что структура документа не хранится целиком в памяти, а вместо этого парсер сообщает о синтаксическом разборе каждого элемента в отдельности. Поскольку этот подход не поддерживается стандартной библиотекой org.json, я воспользовался альтернативой - Jackson JSON. Вот приблизительно как выглядел код после того как я его переписал:
do {
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(url.toString());
HttpResponse response = client.execute(request);

// Pull content stream from response
HttpEntity entity = response.getEntity();
InputStream inputStream = entity.getContent();

JsonFactory f = new JsonFactory();
JsonParser jp = f.createJsonParser(inputStream);
if (jp.nextToken() == JsonToken.VALUE_NULL) {
  break;
}

while (jp.nextToken() != JsonToken.END_ARRAY) {
  Point point = null;

  while (jp.nextToken() != JsonToken.END_OBJECT) {
    String fieldname = jp.getCurrentName();
    jp.nextToken(); // move to value, or
    // START_OBJECT/START_ARRAY
    if ("item".equals(fieldname)) { // contains an object
      point = pointFromJson(jp);
    } else {
      throw new IllegalStateException("Unrecognized field '" + fieldname + "'!");
    }
  }

  if (point != null) {
    db.getPointDao().create(point);
  }
}
jp.close();
А, вот как изменилась статистика:
После трёх замеров, получилось что разборка JSON ускорилась всего на 10-15%. Немного, но кроме этого данные не копируются в памяти несколько раз, поскольку пыборка происходит непосредственно из потока с http-ответом. Оптимизируем дальше. Почему JSON ? На самом деле в этой задаче нет особого смысла использовать JSON, поскольку данные "плоские" и не имеют никакой вложенности. Я решил ещё раз переписать вытягивание данных и на этот раз использовать CSV, поскольку он самый экономичный по размеру. А значит и по скорости приложение должно ощутимо выиграть, поскольку меньше данных будет скачиваться по сети. Итак, вместо 4,3Мб в CSV формате получилось 2,7 Мб что меншье на 37%. Код приводить не буду, он достаточно тривиальный. Скажу только что для работы с csv лучше всего использовать готовую библиотеку opencsv. Что же получилось теперь ? Прирост получился невелик - всего 5%-10%, но это потому что у меня слишком быстрый интернет. В реальных условиях работы через 3G импорт данных будет работать значительно быстрее после таких изменений. Ведь всё-таки почти на 40% меньше данных скачивается. Теперь, когда передача данных рабоатает оптимально, можно заняться оптимизацией вставки объектов в базу данных. Когда выполняется много однотипных операций вставки, то имеет смысл обернуть их в одну транзакцию. Таким образом будет во множество раз меньше обращений к файловой системе для физического сохранения данных, что сэкономит огромное количество времени. Это раз. И вторая, менее значительная оптимизация - использование откомпилированых запросов на вставку. Это два. Улучшенный код вставки данных:
// Prepare insert statement.
SQLiteStatement insert = db.compileStatement(INSERT_STATEMENT); 
db.beginTransaction();

try {
  while (scanner.hasNextLine()) {
    String line = scanner.nextLine();

    String[] columns = line.split(";");

    try {
      if (columns.length == 7) {
        insert.clearBindings();

        // Important ! Order of the fields in the statement
        // should be the same like in CSV input.
        for (int i = 0; i < 7; i++) {
          insert.bindString(i + 1, columns[i]);
        }

        insert.execute();
        result++;
      }
    } catch (Exception e) {
      Log.e("AppTrack", Log.getStackTraceString(e));
    }
  }

  db.setTransactionSuccessful();
} finally {
  db.endTransaction();
}

Итак, после всех-всех оптимизаций импорт данных занимает менее 1,5 мин вместо изначальных 10 мин. Кроме этого данные не копируются несколько раз в памяти, что существенно снижает нагрузку на сборщик мусора и виртуальную машину.

Вот такой небольой case study. Надеюсь кому-нибудь пригодится. Если что - пишите в коменты.

Monday, March 21, 2011

Нагрузочное тестирование веб сервиса с помощью soapUI

Есть такая полезная штука soapUI. Пользуюсь ей давно, но, в основном, только для того чтобы быстро проверить отдельные методы веб сервисов. Сегодня понадобилось провести нагрузочное тестирование веб сервиса авторизации, который я недавно написал.
Задача как бы тривиальная, но, вот самое сложное в такого рода задачах - это выбрать инструмент. Итак приступаем. После получаса общения с поисковой системой у меня насобирался списочек различного рода утилит. И первую, которую я решил попробовать – это младший брат soapUI, под названием loadUI. В общем, я эту штуку скачал, установил, запустил, немного подождал, увидел пользовательский интерфейс и понял что это не то что мне нужно. Интерфейс, не ужасный, очень даже красивый, и, наверное, даже слишком для такой утилиты, просто я его не понял. Половина экрана занимает раздел с устаревшими новостями и всё как-то уж очень медленно работает. Закрыв его подальше, я решил посмотреть нет ли в soapUI того что мне нужно, т.к. где-то краем глаза видел что эти две утилиты интегрируются. И мне повезло - оказывается есть ! И уже давно есть, просто раньше я не обращал внимания на то что когда импортируешь WSDL чудесной галочкой можно скачать чтобы soapUI сгенерировал набор тестов:


И следующим шагом указать настройки для генерации тестов. Для функциональных тестов мне подошёл один тест кейс в котором включены обращения к обоим методам моего веб сервиса.
Внизу самая полезная галочка, это генерация нагрузочного теста на основе функционального.


Вот как выглядит то что получилось в дереве проекта:


Теперь, когда шаблоны тестов созданы, можно приступить к эмуляции логики клиента.


В моём случае всё достаточно просто: клиент через определённые интервалы времени обращается к методу ApproveSession для подтверждения сессии и в конце работы принудительно закрывает свою сессию с помощью TerminateSession. Всё просто, но есть два нюанса:
1. Необходима поддержка HTTP сессии с помощью Cookies
2. Каждый клиент должен присылать свой уникальный ключ на сервер, который в идеале ещё и должен бы быть закриптован

Итак, оказалось, что первый пункт легко разрешается с помощью ещё одной волшебной галочки, а именно:


Разобравшись с первым вопросом я приступил ко второму. После получаса рысканья по просторам интернета, нашёл очень полезную статью, которая реально сэкономила мне немного времени, и теперь я с радостью потрачу его на блог :)
Итак, применительно к моему случаю, SOAP запрос на подтверждение сессии выглядит приблизительно следующим образом:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tem="http://tempuri.org/">
   <soapenv:Header/>
   <soapenv:Body>
      <tem:ApproveSession>
         <tem:token>?</tem:token>
      </tem:ApproveSession>
   </soapenv:Body>
</soapenv:Envelope>

Чтобы обеспечить уникальнсть ключа, я буду использовать индекс потока в котором запускается клиент, выглядит это так:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tem="http://tempuri.org/">
   <soapenv:Header/>
   <soapenv:Body>
      <tem:ApproveSession>
         <tem:token>token${=context.getProperty("ThreadIndex")+1}</tem:token>
      </tem:ApproveSession>
   </soapenv:Body>
</soapenv:Envelope>

Теперь, когда запросы готовы, можно добавить несколько повторов (к сожалению, не знаю как можно организовать цикл в SoapUI, если кто подскажет – буду признателен), задержки между ними и запустить нагрузочный тест:


Итак, в результате запуска теста, получен полезный отчёт. Каждый шаг был выполнен 501 раз (колонка cnt) на протяжении интервала тестирования в одну минуту. Этот интервал я установил перед запуском теста. Кроме того, среднее время отклика сервера в пределах 250 мс (колонка avg), при этом максимальное время отклика порядка 1 сек, а минимальное - 100 мс. Очень неплохо при параллельной работе 500 нагрузочных клиентов. Суммарный объём трафика для всех клиентов менее 1 мб, при этом не было ни одного отказа. В целом результат меня удовлетворил. К сожалению, не получилось сэмулировать большее количество клиентов. При попытке запустить 1000 проходит всего 50 первых запросов а дальше в лог возвращаются ошибки соединения и на короткое время пропадает доступ к интернету. Скорее всего исчерпываются свободные ресурсы на ноутбуке где я проводил тест. На сервер нареканий нет - в логах сервера всё чисто, а счётчики производительности показывают что все запросы были успешно обслужены. Но это уже детали.

Главное, что soapUI всё-таки полезная штука, и в который раз она меня выручает :)

Saturday, February 26, 2011

Я поэт

Есть у меня хорошая идея сделать онлайн бибиотеку поэзии. Вот моя небольшая презентация с Android Hackton, который проходил в Киеве в феврале.

Wednesday, February 23, 2011

Первое приложение на Android Market !

Скажу сразу что это было непросто, но в конце концов я это сделал! И теперь моё первое приложение можно увидеть по ссылке.
Надеюсь в ближайшее поделиться как это было :)

Friday, January 28, 2011

Создание инсталляции с помощью WIX и 7-Zip

Специально для этой статьи, я создал простую инсталляцию. Для этого с помощью визарда я создал Wix проект в Visual Studio и добавил файл license.txt, который будет копироваться в ходе инсталяции. Вот содержимое Product.wxs:
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <Product Id="39ef48c3-d7ca-4947-9644-4146e9d6c1ec" Name="WixExeSetup" Language="1033"
           Version="1.0.0.0" Manufacturer="WixExeSetup"
           UpgradeCode="d69d6b38-b736-44e8-98b8-d3fded2545da">
    
    <Package InstallerVersion="200" Compressed="yes" />
    <Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />
    <UIRef Id="WixUI_Minimal"/>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="INSTALLLOCATION" Name="WixExeSetup">
          <Component Id="license.txt" Guid="177b4634-0246-49db-89a9-c3b539ebd93d">
            <File Id="license.txt" Source=".\license.txt" KeyPath="no" />
          </Component>
        </Directory>
      </Directory>
    </Directory>
    <Feature Id="ProductFeature" Title="WixExeSetup" Level="1">
      <ComponentRef Id="license.txt" />
    </Feature>
  </Product>
</Wix>

Если скомпилировать этот проект, то будет получен инсталляционный msi-пакет. Что если я захочу инсталляцию не в виде .msi, а в виде .exe (неважно из каких соображений)? Оказывается, что WIX в текущей версии этого не позволяет. Итак, после пары часов поиска, я пришёл к тому что проще всего поместить .msi файл в самораспаковывающийся (sfx) архив, после запуска которого, содержимый .msi будет распаковываться во временную папку и затем запускаться.

Для создания sfx-архива я использовал 7-Zip 9.20. Я скачал сам архиватор, а также SFX Plugin.

После этого, я установил 7-Zip и распаковал SFX Plugin в папку с 7-Zip. Чтобы была возможность использовать архиватор из командной строки во время компиляции универсальным способом я также добавил путь к папке с 7-Zip в переменную окружения PATH.

Теперь, чтобы в процессе сборки проекта инсталляция упаковывать в sfx архив в post-build action нужно добавить следующую команду:
7z a -y -sfx7zS2.sfx $(TargetName).exe $(TargetFileName)

В результате, после сборки проекта кроме .msi файла собирается также .exe файл, который содержит запакованный msi.

Теперь подробнее о том что происходит, когда запускается полученный .exe файл:

  1. Содержимое архива (.msi в нашем случае) распаковывается во временную директорию
  2. Из временной директории запускается .msi файл
  3. По окончанию инсталляции временная директория со всем содержимым удаляется

Следует заметить, что размер полученного архива в виде .exe файла в несколько раз меньше размера исходной инсталляции. В данном примере .exe файл в 2,5 раза меньше. Хоть это и небыло моей целью, т.к. мне была нужна инсталляция в виде exe-шника, этот "побочный" эффект весьма приятен :)

P.S.
В Wix 3.6 появилась тулза burn, с помощью которой можно создавать загрузчики (bootstrapper) для инсталляционных пакетов. Но, к сожалению, на момент написания статьи, версия Wix 3.6 находится в стадии разработки.

Скачать исходный код

Tuesday, January 18, 2011

Загрузка исходников Android под Windows или как я познакомился Git

Для контроля версий исходного кода Android используется Git. В общих чертах Git - это распределённая система контроля версий, где каждая локальная копия является обособленным репозиторием. Такая архитектура позволяет контролировать локальные изменения, даже если нет связи с удалённым центральным репозиторием.

Сразу оговорюсь что это моё первое знакомство с Git. До этого я работал только с TFS, CVS и преимущественно с SVN.

Что мне больше всего нравится в Android, это то что его код открыт и доступен каждому в сети. Исходный код, рекомендации по работе с ним а также множество связанной информации находится на сайте http://source.android.com. Для ознакомления непосредственно с кодом существует Web-интерфейс, который называется GitWeb. Чтобы его увидеть нужно перейти по ссылке http://android.git.kernel.org. При этом на страничке появится длинный список из репозиториев. Для каждого подпроекта создан отельный репозиторий, поэтому их так много. Такая организация обусловлена тем, что Git умеет работать только со всем репозиторием целиком, но никак не с отдельным его подкаталогами, как это делает SVN. И если бы исходный код Android, а он занимает порядка 2 Gb, хранился в одном репозитории, то даже если нужен только код одного небольшого приложения потребовалось скачать полностью все 2 Gb исходников.

Итак, перейдя по ссылкам в разные репозитории можно увидеть историю изменений, файловую структуру, а также посмотреть содержимое каждого файла. Этот интерфейс вполне подходит если нужно что-то глянуть, но вот если доступа к сети нет или хочется поэксперементировать с исходниками, то просто необходимо иметь их копию у себя на машине.

На моём рабочем ноутбуке стоит Windows, поэтому чтобы скачать исходники мне понадобилось установить msysgit (Git клиент для Windows) и затем по-порядку:


  1.  Запустить Git Bash из меню Пуск или меню проводника
  2. mkdir c:/android - создать папку для загружаемого проекта
  3. cd c:/android - перейти в эту папку
  4. git clone http://android.googlesource.com/platform/packages/apps/DeskClock - клонирование репозитория с приложением DeskClock

Теперь исходный код приложения DeskClock находится в папке c:/android/DeskClock и я могу работать с ним локально.

Как альтернатива работе с msysgit клентом через командную строку есть возможность использовать GUI и команды контекстного меню из проводника, но мне они показались скудноватыми и не очень интуитивно-понятными. Поэтому я усатновил альтернативную оболочку TortoiseGit, которая очень похож на старый добрый TortoiseSVN и жизнь по-тихоньку начала налаживаться :) Чуть не забыл TortoiseGit обязательно необходимо наличие msysgit, иначе он будет выдавать ошибку что тот не найден.

И напоследок, поскольку репозиториев много, порядка 260, то чтобы скачать весь исходный код Android не очень удобно будет выкачивать их по-одному. Чтобы упростить задачу можно воспользоваться скриптом repo о котором упоминается здесь, но поскольку он написан на Python, то потребуется ещё устанавливать MinGW или что-то в этом духе. Как альтернатива есть обычный batch скрипт, который можно позаимствовать отсюда, однако со временем он может устареть, поскольку содержит захардкоженые адреса репозиториев.

EDIT 16/04/2012: Со времени написания статьи онлайн браузер исходников http://source.android.com перестал поддерживаться проектом Android. Есть по крайней мере три альтернативы быстро посмотреть код:
  1. GrepCode - очень удобный просмотровик кода. Простое переключение по веткам и тегам. Отображение Outline классов, как в Eclipse. Хорошо гуглится. Достаточно вбить GrepCode + <имя класса> в гугл.
  2. Сode Aurora - старый gitweb интерфейс, который хостится Code Aurora mobile open source community
  3. Также можно воспользоваться старым добрым GitHub, где хранится зеркало основных репозиториев

Для скачивания исходников целиком рекомендуется использовать утилиту repo. Как это сделать подробно описано здесь.