|
JAVA Micro Edition Низкоуровневое программирование мобильных телефонов. |
18.12.2014, 09:56
|
#16
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Загрузка советов - "лишние" 400 кб RAM
В проге есть советы, которые показываются в окне "ожидайте...", пока грузится страница браузера.
В данный момент советов много, суммарное количество символов текста около 5900. т.е. примерно 5900*4 = 23,6 кб.
Есть версионность, т.е. советы присылаются из сокета только если в клиенте устаревшая версия. Иначе достаём из кэша.
Выяснил вот что.
1. До загрузки советов занято 1130 кб оперативки
2. Каждый совет в среднем жрёт 1-2 кб
3. На момент загрузки всех советов из сокета занято 1205 кб
4. После завершения функции tipsPrepare() занято 1548 кб
5. После очередного вызова сборщика мусора занято 1110 кб
Принцип добавления советов
1. Каждый совет присылается в отдельной команде
2. При получении команды добавляем совет в список (LinkedList)
3. Добавляем все советы
4. Создаём строковый массив tips, из которого будем рандомно выцеплять совет для показа
5. В цикле проходим по списку с советами, заполняя массив и формируя длинную строку, содержащую все советы, чтобы далее сохранить эту строку в кэш (внутр. память)
6. чистим список советов (хотя это особо ничего не даёт, т.к. ссылки на строки есть в массиве)
Самое прожорливое место - формирование строки для сохранения в кэш. Опять это "дурацкое" пересоздание массива chars внутри StringBuffer'a при сложении строк.
tipsString = "";
for(listTips.start(); listTips.hasElement(); listTips.next(), ++k) {
tips[k] = (String)listTips.item();
if(tipsString.length() > 0)
tipsString += "\r\n";
tipsString += tips[k];
}
После этого цикла оперативка доходит до 1496 кб.
Далее идёт сохранение в кэш, которое и доводит до финального уровня 1548 кб.
Решение, оптимизация
Сделал переменную int tipsStrLen, в которую плюсую длину каждого совета при его добавлении в список, и юзаю StringBuffer заранее указанной длины:
StringBuffer buf = new StringBuffer(tipsStrLen);
int k = 0;
for(listTips.start(); listTips.hasElement(); listTips.next(), ++k) {
tips[k] = (String)listTips.item();
if(k > 0) {
buf.append("\r\n");
}
buf.append(tips[k]);
}
tipsString = buf.toString(); //получаем строку для сохранения в кэш
В результате имеем:
* tips.prepared: 1218 кб
* tips. saved: 1269 кб (тут без изменений, главное что суммарно занято теперь намного меньше)
Ура, товарищи!
Замечания:
1. Советы меняются не слишком часто, т.е. обычно грузятся из кэша, при этом такой перерасход памяти не наблюдается. В основном грузятся при первом входе в чат, но ничего не мешает получить список команд при любом очередном перелёте на планету.
2. Храню в кэше всё одной строкой через \r\n, при загрузке разбиваю строку на отдельные советы. Возможно, лучше хранить отдельно каждый совет.
3. При каждом перелёте на планету советы заново создавались из строки, взятой из кэша, это память почти не расходует, но это лишняя ненужная операция *. Тоже поправил.
* время разбивки строки на компе неинформативно, а на мобилке замерять лень.
|
(Offline)
|
|
Сообщение было полезно следующим пользователям:
|
|
13.01.2015, 09:35
|
#17
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Замутил удобоваримые для мозга сортировки образов внутри объектов.
Образы сортируются сначала по глубине depth - это слой рисования: -1 задний, 0 средний, 1 передний; затем по addOrder - порядку добавления образа в объект.
Т.к. я отказался от массивов в пользу списков, то пришлось переделывать сортировку.
Вот что получилось.
LinkedList.sortBubble()
public void sortBubble(final IComparator comparator) {
if(count < 2) { return; }
boolean dirty = true;
while(dirty) {
Link node1 = first;
Link node2 = node1.next;
dirty = false;
while(true) {
int r = comparator.compare(node1.item, node2.item);
if(r < 0) {
Object tmp = node1.item;
node1.item = node2.item;
node2.item = tmp;
dirty = true;
}
node1 = node2;
node2 = node2.next;
if(node2 == null) {
break;
}
}
}
}
Было:
public static void sortViews_Depth_AddOrder(final View views[]) {
if(views.length < 2) { return; }
int max;
int val, prev=0;
View tmp;
final int mas[] = new int[views.length];
int index = 0;
//сортировка по depth
for(int k = 0 ; k < views.length ; ++k) {
max = views[k].depth();
for(int j = k+1 ; j < views.length ; ++j) {
val = views[j].depth();
if(val > max) {
tmp = views[j];
views[j] = views[k];
views[k] = tmp;
max = val;
}
}
if(k > 0 && max != prev)
++index;
++mas[index];
prev = max;
}
//сортировка по addOrder
index = 0;
int end = 0;
for(int k = 0 ; k < views.length ; ++k , ++index) {
if(mas[index] < 2) //если один с таким значением, то он уже на своём месте
continue;
end = k+mas[index];
for(int j = k ; j < end ; ++j) {
max = views[j].addOrder();
for(int i = j+1 ; i < end ; ++i) {
val = views[i].addOrder();
if(val > max) {
tmp = views[i];
views[i] = views[j];
views[j] = tmp;
max = val;
}
}
}
}
}
здесь в массив mas (писал ещё в те времена, когда массивы называл mas, а не arr) накапливаем количество одинаковых чисел, идущих подряд, чтобы потом отсортировать среди них по addOrder.
рекламный слоган: "Тройной FOR - это по-нашему!"
Стало:
private static DepthAddOrderComparator depthAddOrderComparator;
public static void sortViews_Depth_AddOrder(final LinkedList views) {
if(views.count() < 2) { return; }
if(depthAddOrderComparator == null) {
depthAddOrderComparator = new DepthAddOrderComparator();
}
views.sortBubble(depthAddOrderComparator);
}
А вот наш чудесный "комплексный" компаратор, который включает в себя сравнение по двум параметрам - depth и addOrder.
public class DepthAddOrderComparator implements IComparator {
public int compare(Object o1, Object o2) {
ViewObject vo1 = (ViewObject)o1;
ViewObject vo2 = (ViewObject)o2;
View v1 = vo1.view();
View v2 = vo2.view();
int d1 = v1.depth();
int d2 = v2.depth();
if(d1 == d2) {
//если глубина одинакова, то нужно сортировать по очерёдности добавления
int a1 = v1.addOrder();
int a2 = v2.addOrder();
if(a1 == a2) {
return 0;
} else if(a1 > a2) {
return 1;
} else {
return -1;
}
} else if(d1 > d2) {
return 1;
} else {
return -1;
}
}
}
Добавление условий
if(d1 == d2) {
//если глубина одинакова, то нужно сортировать по очерёдности добавления
int a1 = v1.addOrder();
int a2 = v2.addOrder();
if(a1 == a2) {
return 0;
} else if(a1 > a2) {
return 1;
} else {
return -1;
}
}
позволяет избавиться от монстра в виде
//сортировка по addOrder
index = 0;
int end = 0;
for(int k = 0 ; k < views.length ; ++k , ++index) {
if(mas[index] < 2) //если один с таким значением, то он уже на своём месте
continue;
end = k+mas[index];
for(int j = k ; j < end ; ++j) {
max = views[j].addOrder();
for(int i = j+1 ; i < end ; ++i) {
val = views[i].addOrder();
if(val > max) {
tmp = views[i];
views[i] = views[j];
views[j] = tmp;
max = val;
}
}
}
}
|
(Offline)
|
|
Эти 2 пользователя(ей) сказали Спасибо Жека за это полезное сообщение:
|
|
13.01.2015, 21:23
|
#18
|
Бывалый
Регистрация: 22.08.2006
Сообщений: 700
Написано 146 полезных сообщений (для 267 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Не думаю, что очень часто возникает ситуация, когда вектор расширяется и потом не заполняется элементами. Т.е. выигрываешь в памяти не часто, а вот в скорости может быть заметный проигрыш, т.к. вектор быстрее списка, даже на операциях вставки в середину не очень больших объектов. А всё потому что кэш процессора (данные лежат последовательно в памяти). Для справки цитата из презенташки характерное время доступа к памяти в тиках процессора:
1 cycle to read a register
4 cycles to reach to L1 cache
10 cycles to reach L2 cache
75 cycles to reach L3 cache
and hundreds of cycles to reach main memory.
Т. е. я бы потестировал еще внимательно работу вектора и списка.
|
(Offline)
|
|
Эти 2 пользователя(ей) сказали Спасибо MiXaeL за это полезное сообщение:
|
|
13.01.2015, 21:41
|
#19
|
Мастер
Регистрация: 03.05.2010
Адрес: Подмосковье
Сообщений: 1,218
Написано 438 полезных сообщений (для 790 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
В стандартном Vector каждый метод synchronized - теоретически, из-за этого может быть медленнее.
А вообще, можно посмотреть исходники и на их основе написать свою версию, заточенную под требования конкретного случая.
__________________
О¯О ¡¡¡ʁɔvʎнdǝʚǝdǝu dиW
|
(Offline)
|
|
Сообщение было полезно следующим пользователям:
|
|
14.01.2015, 06:38
|
#20
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Сообщение от MiXaeL
Не думаю, что очень часто возникает ситуация, когда вектор расширяется и потом не заполняется элементами. Т.е. выигрываешь в памяти не часто, а вот в скорости может быть заметный проигрыш, т.к. вектор быстрее списка, даже на операциях вставки в середину не очень больших объектов. А всё потому что кэш процессора (данные лежат последовательно в памяти).
|
1. если вектору не указать capacity increment (а я никогда его не указываю), то при расширении он станет в 2 раза больше. поэтому шанс не заполниться при добавлении 1 объекта - большой
2. вставка в середину. в джаве есть arrayCopy, сдвигается с его помощью, наверное поэтому шустро.
я обычно вставляю в список или удаляю через ссылку на узел (ListNode), т.е. по какому-то условию в цикле, когда у меня есть этот самый узел,
а это быстро, т.к. нужно просто переприсвоить ссылки next, prev
for (views.start(); views.hasElement(); views.next()) { ViewObject v = (ViewObject)views.item(); if(v.id() == id) { views.remove(views.node()); break; } }
3. как обстоят дела с кэшем в мобилках я мало представляю, наверное тоже что-то есть
Минус списка - создаётся мелочёвка в виде узлов ListNode для каждого элемента, сборщику мусора нужно будет чистить эти узлы после удаления элементов.
Igor, спасибо что напомнил про synchronized.
|
(Offline)
|
|
Сообщение было полезно следующим пользователям:
|
|
15.01.2015, 14:56
|
#21
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Решил переделать циклы перебора списков.
Было так:
for(list.start() ; list.hasElement() ; list.next()) { CustomObject item = (CustomObject)list.item(); //что-то делаем с объектом item }
т.е. тут есть завязка на внутренний бегунок current внутри списка, который движется от начала списка first, до конца списка current == null.
такой код потенциально опасен - если в цикле обхода списка вызывается функция, которая должна пробегать по этому же списку, то бегунок current становится неправильным.
для решения я сделал (залипуху/костыль/______) в виде дополнительной переменной storedCurrent и два метода store/restore, полагая, что более глубокой вложенности не будет.
глубже вложенности пока нет, но нафиг надо. лишнее поле, работающее не универсально (вот если бы был стек сохраняемых бегунков, то другое дело), и нужное лишь для одного места в коде.
Стало так:
for(ListNode node = list.first(); node != null; node = node.next) { CustomObject item = (CustomObject)node.item; //что-то делаем с объектом item }
что тут скажешь - классика.
проход в обратном порядке делается аналогично, node = list.last();
node.next, node.item - да, они public
Примечание: иногда нужно пропустить очередной next, в этом случае можно либо писать так:
for(ListNode node = list.first(); node != null; /**/) { CustomObject item = (CustomObject)node.item; //что-то делаем с объектом item if(condition) continue;
node = node.next; }
или с помощью вспомогательных функций next и skipNext.
for(ListNode node = list.first(); node != null; node = list.next(node)) { CustomObject item = (CustomObject)node.item; //что-то делаем с объектом item if(condition) list.skipStep(); }
...... //LinkedList.java
public void skipStep() { isNeedSkipStep = true; } public ListNode next(final ListNode node) { if(isNeedSkipStep) { isNeedSkipStep = false; return node; } return node.next; }
Доселе я в некоторых местах использовал skip.
Теперь хочу отказаться от этого, чтобы не было путаницы.
пример: вызываешь в цикле метод list.skipStep(), надеешься что он не подведёт,
а в условии цикла про это забываешь, делая простой node = node.next;
UPD: большинство списков требуют только однопаправленный проход.
Есть мысль сделать односвязный список и юзать его по максимуму.
Неудобство - функция удаления узла, т.к. из текущего узла никак не выцепить предыдущий, чтобы перекинуть дальше его ссылку next.
Скорее всего сделаю этот список, в функцию удаления буду передавать два узла - подлежащий_удалению и предыдущий_для_него.
Удалений элементов не слишком много, все они в цикле, т.е. можно легко хранить внешний указатель на prev.
|
(Offline)
|
|
15.02.2015, 19:20
|
#22
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Переписал HTML- и JSON- парсеры
У нас в проекте используется псевдо-html. Часть тегов соответствуют обычному хтмл, часть - наши самодельные теги.
В инете я нашёл парсер json для j2me. Но он мне показался слишком громоздким, дофигища классов.
В итоге я написал свой, всего 100 строк кода (со второй попытки, первая попытка была страшновата для неподготовленного мозга).
Скорость работы и потребляемую память не сравнивал с "интернетовским".
Минус - юзаю рекурсию. Но мы большую вложенность не предполагаем юзать, а каких-нибудь 10 уровней внутрь мобилки тянут.
А вот html парсер я сравнивал со своим же, который юзаю в текущей версии. Пока что сравнение сделал для страницы регистрации (другие популярные страницы - на очереди).
текущий
время: 29 мс
память: 20 кб
новый
время: 12-15 мс
память: 13 кб
Сравнивал на ПК-эмуляторе, это конечно не качественно. Потом проверю на мобильнике.
Ещё из фишек нового парсера - можно делать неограниченную вложенность тегов.
Текущий не умеет делать вложенные однотипные теги, например таблицу в таблицу нельзя или span в span. Теперь можно.
Ну и новый тег style добавили, теперь можно стили юзать, это должно сократить код, облегчить вёрстку.
|
(Offline)
|
|
24.04.2015, 10:57
|
#23
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Уже давненько открыл для себя в нетбинсе фичу автоподстановки, которая предлагает варианты сразу при написании первой буквы.
По дефолту эта фича выключена.
Очень ускоряет кодинг.
Скрин.
|
(Offline)
|
|
Сообщение было полезно следующим пользователям:
|
|
25.06.2015, 20:33
|
#24
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Мы зарелизили новую версию - Galaxy 9.0!
Не сегодня, пораньше. Вчера я уже допилил багфиксы для 9.0.4.
К делу. На некоторых телефонах прога закрывалась сразу при запуске, выдавая NullPointerException.
Включая 2 наших тестовых телефона, и это хорошо - было на чём разбираться. Сроки горели, пришлось с багом релизить.
(Но мы не афишировали обнову сразу, лишь спустя несколько дней), так что не было поголовных апдейтов.
Тем более что это j2me, нет автообновлений.
Жалобы про нулпоинтер, конечно, посыпались в службу поддержки.
Начал разбираться.
Встал вопрос - как это дебажить. В андроиде халява - по шнуру тебе в дебаг сливается стек с ошибкой, видишь откуда корни,
а на j2me хрен, - даже текстом на экран просто так не выведешь,
т.к. при появлении ошибки вылазит системный алерт и ничего не успеешь понять.
В итоге я сделал функцию waitForKey, которая крутит бесконечный цикл, пока не нажмём клавишу.
Благо клавиши обрабатываются в отдельном системном потоке, и мы их можем ловить при наличии бесконечного цикла в главном потоке.
Комбинируя waitForKey с выводом на экран каждого шага загрузки ресурсов, я обнаружил,
что проблема в функции загрузки текста из файла:
public static String loadText(final String path) {
try {
final InputStream is = Utils.class.getResourceAsStream(path);
final DataInputStream dis = new DataInputStream(is);
int size = dis.available();
byte[] b = new byte[size];
int actual = dis.read(b);
dis.close();
is.close();
String s = (new String(b, 0, actual, "UTF-8")).trim();
return s;
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
Изучил её внутренности, вроде всё в порядке, но я выдвинул гипотезу-причину, переделал, но не помогло.
[ гипотеза: некоторые телефоны выдают некорректную длину данных в потоке: int size = dis.available();]
Дальнейший пошаговый дебаг выявил окончательную причину.
Так в чём же тут дело?
Ответ:
Выполнение функции попадало в catch с типом IOException, на строчке закрытия стрима, dis или is - кого именно я выяснять не стал.
Сделал вывод: некоторые телефоны, закрывая DataInputStream, автоматом закрывают и InputStream.
Решение: закрывать стримы в секции finally, оборачивая снова в try...catch....
Попутно узнал, что в конструкции try - catch - finally секция finally выполнится, даже если в try есть досрочный return.
private boolean abcd() {
try {
Midlet.sout("try to do");
return true;
} catch (Exception e) {
Midlet.sout("exception");
} finally {
Midlet.sout("finally"); // этот код выполняется
}
return false;
}
|
(Offline)
|
|
Сообщение было полезно следующим пользователям:
|
|
03.12.2015, 12:54
|
#25
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Узнал интересную штуку для дебага - определение вложенности вызовов функций:
try {
throw new Exception("test");
} catch (Exception e) {
e.printStackTrace();
}
Генерируем исключение и печатаем его стек.
Блин, это гениально!
Помогло мне разобраться в длинной череде вызовов:
java.lang.Exception: test
at galaxy.browser.Page.imageAddToDownloads(Page.java:1463)
at galaxy.browser.ElementImage.prepare(ElementImage.java:165)
at galaxy.browser.Element.prepare(Element.java:287)
at galaxy.browser.ElementFactory.updateParams(ElementFactory.java:269)
at galaxy.browser.ElementFactory.insert(ElementFactory.java:190)
at galaxy.browser.ElementFactory.insert(ElementFactory.java:183)
at galaxy.browser.ElementFactory.createElementImage(ElementFactory.java:941)
at galaxy.browser.ElementFactory.createElement(ElementFactory.java:72)
at galaxy.browser.ElementFactory.fillContainer(+113)
at galaxy.browser.ElementFactory.fillContainer(+6)
at galaxy.browser.ElementSpan.prepare(ElementSpan.java:48)
at galaxy.browser.ElementFactory.updateParams(ElementFactory.java:269)
at galaxy.browser.ElementPlank.updateInnerElements(ElementPlank.java:153)
at galaxy.browser.ElementPlank.prepareInners(ElementPlank.java:77)
at galaxy.browser.ElementPlank.layout(ElementPlank.java:164)
at galaxy.browser.ElementContainer.layout(ElementContainer.java:243)
at galaxy.browser.ElementTable.layout(ElementTable.java:432)
at galaxy.browser.ElementContainer.layout(ElementContainer.java:243)
at galaxy.browser.ElementPages.showPage(ElementPages.java:265)
at galaxy.browser.ElementPages.activate(ElementPages.java:223)
at galaxy.browser.ElementPages.layout(ElementPages.java:328)
at galaxy.browser.ElementContainer.layout(ElementContainer.java:243)
at galaxy.browser.Page.layout(Page.java:640)
at galaxy.browser.Page.create(Page.java:172)
at galaxy.browser.Browser.createPage(+34)
at galaxy.browser.Browser.createPage(+48)
at galaxy.browser.Browser.createPage(+6)
at galaxy.browser.Browser.loadPageOffline(+28)
at galaxy.Main.browserShowOffline(+39)
at galaxy.Main.showTestPage(+136)
at galaxy.Main.run(+94)
|
(Offline)
|
|
Эти 3 пользователя(ей) сказали Спасибо Жека за это полезное сообщение:
|
|
03.02.2016, 09:52
|
#26
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Прокрутка браузерных страниц.
Раньше у меня при прокрутке страницы делался сдвиг всех элементов формы.
В итоге для жирных страниц совершалось много теледвижений, проходов по вложенным тегам.
Теперь просто сдвигаю канвас при отрисовке страницы.
Дополнительно пришлось
* поправить функцию setClip, т.к. при сдвиге канваса область отсечения также уезжает.
* поправить пункцию isElementInView, т.к. раньше в ней завязка была на то что меняем координаты самих элементов.
|
(Offline)
|
|
08.02.2016, 14:23
|
#27
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
С функциями graphics.setClip() и graphics.translate() оказалось больше проблем, чем хотелось бы.
Прокачал класс Viewport - хэлпер для работы с клиппингом. Н-р, он умеет делать lock/unlock, чтобы последующие установки клипа не выходили за эту залоченную область.
Что узнал:
1. Важен порядок вызова функций
setClip => translate != translate => setClip
при установке области клипа учитывается текущий сдвиг координат, но все последующие никак не влияют, запоминает именно сдвиг в момент установки клипа.
2. Реальные координаты клипа g.getClipX() и g.getClipY() - возвращаются значения с учётом сдвига координат.
3. g.translate(int x, int y) - это не передвинуть канвас в указанную точку, а сдвинуть на указанное количество относительно текущей позиции. могли бы написать в доке dx, dy для понятности.
4. Внутренности графикса по части клипа и транслэйта сложнее, чем думал (см. скрин)
|
(Offline)
|
|
05.05.2016, 07:19
|
#28
|
Дэвелопер
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений (для 886 пользователей)
|
Ответ: Смесь: Неочевидное + Оптимизация
Если кто-то использует NetBeans IDE и любит тёмное оформление, то можете установить Darcula плагин.
https://github.com/Revivius/nb-darcula
Он делает тёмным весь интерфейс, а не только область кода, как некоторые.
Портал с плагинами недоступен, пришлось компилить из исходников.
скачать из дропбокса nb-darcula-1.5.nbm
|
(Offline)
|
|
Сообщение было полезно следующим пользователям:
|
|
Ваши права в разделе
|
Вы не можете создавать темы
Вы не можете отвечать на сообщения
Вы не можете прикреплять файлы
Вы не можете редактировать сообщения
HTML код Выкл.
|
|
|
Часовой пояс GMT +4, время: 02:46.
|